dok/public/js/gaststaette/glucksspiel.js
2026-04-10 12:29:23 +01:00

699 lines
25 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/gaststaette/glucksspiel.js
Glücksspiel-Tab in der Gaststätte
Karten nach Fraktion anzeigen (wie Kartendeck)
Karten gleicher ID ins Kombinier-Feld legen
"Kombinieren"-Button
============================================================ */
const CARDS_PER_PAGE = 18;
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);
}
// State
let gs_groupId = null;
let gs_page = 1;
let gs_cards = [];
let gs_combineId = null;
let gs_combineCards = [];
let gs_initialized = false;
let gs_tavernLevel = 1;
let gs_required = 10; // Karten für 100%
function getRequired(tavernLevel) {
if (tavernLevel < 5) return 10;
if (tavernLevel < 10) return 8;
if (tavernLevel < 15) return 6;
if (tavernLevel < 20) return 5;
return 3;
}
function getChance(amount, required) {
return Math.min(Math.round((amount / required) * 100), 100);
}
/* ══════════════════════════════════════════════
INIT
══════════════════════════════════════════════ */
export async function loadGlucksspiel() {
const container = document.getElementById("gs-tab1");
if (!container) return;
// Nur beim ersten Aufruf rendern
if (gs_initialized) return;
gs_initialized = true;
container.innerHTML = renderShell();
attachCombineEvents();
try {
// Gaststätte-Level und benötigte Karten laden
const infoRes = await fetch("/api/cards/combine-info");
if (infoRes.ok) {
const info = await infoRes.json();
gs_tavernLevel = info.tavernLevel;
gs_required = info.required;
}
const res = await fetch("/api/card-groups");
if (!res.ok) throw new Error();
const groups = await res.json();
renderTabs(groups);
if (groups.length) {
gs_groupId = groups[0].id;
await loadCards();
}
} catch {
container.innerHTML = `<p style="color:#8b6a3c;padding:20px;font-family:'Cinzel',serif;">Fehler beim Laden.</p>`;
}
}
/* ══════════════════════════════════════════════
SHELL
══════════════════════════════════════════════ */
function renderShell() {
return `
<div class="gs-wrap">
<!-- Linke Tab-Leiste (Fraktionen) -->
<aside class="gs-faction-tabs" id="gs-faction-tabs">
<div class="gs-loading">Lade...</div>
</aside>
<!-- Mittlere Spalte: Karten-Sammlung -->
<div class="gs-collection">
<div class="gs-col-header">Meine Karten</div>
<div class="gs-grid" id="gs-grid">
<div class="gs-loading">Lade Karten...</div>
</div>
<div class="gs-pagination" id="gs-pagination"></div>
</div>
<!-- Rechte Spalte: Kombinier-Feld -->
<div class="gs-combine-panel">
<div class="gs-col-header">Kombinieren</div>
<div class="gs-combine-hint" id="gs-combine-hint">
Klicke auf eine Karte links um sie hier zu platzieren.<br>
Nur Karten gleicher Art können kombiniert werden.
</div>
<div class="gs-combine-grid" id="gs-combine-grid"></div>
<div class="gs-combine-count" id="gs-combine-count"></div>
<button class="gs-combine-btn" id="gs-combine-btn" disabled>Kombinieren</button>
</div>
</div>
<style>
/* ── Layout ─────────────────────────────── */
.gs-wrap {
display: flex;
width: 100%;
height: 100%;
font-family: "Cinzel", serif;
overflow: hidden;
}
/* ── Fraktions-Tabs (ganz links) ─────────── */
.gs-faction-tabs {
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px 10px;
background: rgba(0,0,0,0.3);
border-right: 2px solid #6b4b2a;
min-width: 140px;
flex-shrink: 0;
overflow-y: auto;
}
.gs-tab {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
background: linear-gradient(135deg, #2a1a08, #1a0f04);
border: 2px solid #6b4b2a;
border-radius: 8px;
color: #c8a86a;
font-family: "Cinzel", serif;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-align: left;
white-space: nowrap;
}
.gs-tab:hover { color: #f0d9a6; filter: brightness(1.2); }
.gs-tab.gs-tab-active {
color: #fff !important;
box-shadow: inset 0 0 10px rgba(0,0,0,0.5), 0 0 14px rgba(200,160,60,0.3) !important;
}
.gs-tab-dot {
width: 10px; height: 10px;
border-radius: 50%; flex-shrink: 0;
border: 1px solid rgba(255,255,255,0.3);
}
/* ── Mittlere Spalte: Sammlung ───────────── */
.gs-collection {
flex: 1;
display: flex;
flex-direction: column;
border-right: 2px solid #6b4b2a;
overflow: hidden;
}
.gs-col-header {
font-family: "Cinzel", serif;
font-size: 14px;
font-weight: 700;
color: #f0d9a6;
letter-spacing: 1px;
text-transform: uppercase;
padding: 10px 16px;
background: linear-gradient(#3a2810cc, #1a0f04cc);
border-bottom: 2px solid #6b4b2a;
flex-shrink: 0;
}
.gs-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
overflow-y: auto;
padding: 10px 12px;
align-content: start;
}
/* ── Karte ───────────────────────────────── */
.gs-card {
position: relative;
border: 2px solid #6b4b2a;
border-radius: 8px;
overflow: visible;
background: #1a0f04;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
aspect-ratio: 3/4;
display: flex;
flex-direction: column;
}
.gs-card:hover {
transform: scale(1.06) translateY(-4px);
border-color: #f0d060;
box-shadow: 0 8px 24px rgba(0,0,0,0.7), 0 0 14px rgba(200,160,60,0.3);
z-index: 10;
}
.gs-card.gs-card-disabled {
opacity: 0.4;
cursor: not-allowed;
}
.gs-card.gs-card-disabled:hover {
transform: none;
box-shadow: none;
border-color: #6b4b2a;
}
.gs-card.gs-card-selected {
border-color: #f0d060;
box-shadow: 0 0 14px rgba(240,208,96,0.5);
}
.gs-card img {
width: 100%; height: 100%;
object-fit: cover; display: block;
border-radius: 6px 6px 0 0;
}
.gs-stat-atk {
position: absolute; right: 8px; top: 38%;
transform: translateY(-50%);
background: rgba(180,40,20,0.88);
border: 1px solid #ff6040;
border-radius: 45px; color: #fff;
font-family: "Cinzel", serif; font-size: 9px; font-weight: bold;
padding: 2px 4px; z-index: 5;
}
.gs-stat-def {
position: absolute; left: 8px; top: 38%;
transform: translateY(-50%);
background: rgba(20,80,180,0.88);
border: 1px solid #4090ff;
border-radius: 45px; color: #fff;
font-family: "Cinzel", serif; font-size: 9px; font-weight: bold;
padding: 2px 4px; z-index: 5;
}
.gs-card-footer {
position: absolute; bottom: 4px; left: 6px;
z-index: 5; pointer-events: none;
}
.gs-count-owned {
font-family: "Cinzel", serif; font-size: 12px;
color: #000; font-weight: bold;
text-shadow: 0 0 3px rgba(255,255,255,0.6);
}
.gs-rarity {
position: absolute; top: 72%; left: 0; right: 0;
display: flex; justify-content: center; flex-wrap: wrap;
gap: 1px; padding: 0 4px;
pointer-events: none; z-index: 4;
}
/* ── Pagination ──────────────────────────── */
.gs-pagination {
display: flex; align-items: center; justify-content: center;
gap: 5px; padding: 8px 10px; flex-shrink: 0;
border-top: 1px solid #3a2810;
}
.gs-page-btn {
background: linear-gradient(#3a2810, #1a0f04);
border: 1px solid #8b6a3c; border-radius: 5px;
color: #f0d9a6; font-family: "Cinzel", serif;
font-size: 11px; padding: 3px 10px; cursor: pointer; transition: 0.15s;
}
.gs-page-btn:hover { border-color: #f0d060; }
.gs-page-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.gs-page-btn.gs-page-active {
background: linear-gradient(#6b4b2a, #3c2414);
border-color: #f0d060; color: #fff5cc;
}
/* ── Rechte Spalte: Kombinier-Panel ──────── */
.gs-combine-panel {
width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0,0,0,0.15);
}
.gs-combine-hint {
padding: 14px 16px;
font-family: "Cinzel", serif;
font-size: 11px;
color: #8b6a3c;
line-height: 1.7;
border-bottom: 1px solid #3a2810;
flex-shrink: 0;
}
.gs-combine-grid {
flex: 1;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 20px 12px 10px;
overflow: hidden;
}
/* Gestapelter Kartenstapel */
.gs-card-stack {
position: relative;
width: 240px;
aspect-ratio: 3/4;
cursor: pointer;
}
.gs-stack-card {
position: absolute;
width: 100%;
aspect-ratio: 3/4;
border: 2px solid #4a8b2a;
border-radius: 8px;
overflow: hidden;
background: #0f1a04;
transition: transform 0.2s, border-color 0.2s;
}
.gs-stack-card img {
width: 100%; height: 100%;
object-fit: cover; display: block;
}
.gs-card-stack:hover .gs-stack-card:last-child {
border-color: #ff4444;
}
.gs-card-stack:hover .gs-stack-card:last-child::after {
content: "✕ Entfernen";
position: absolute; inset: 0;
background: rgba(180,0,0,0.6);
display: flex; align-items: center; justify-content: center;
font-family: "Cinzel", serif; font-size: 11px;
color: #fff; border-radius: 6px; pointer-events: none;
}
.gs-combine-count {
padding: 6px 12px;
font-family: "Cinzel", serif; font-size: 12px;
color: #f0d9a6; text-align: center;
border-top: 1px solid #3a2810;
flex-shrink: 0;
min-height: 28px;
}
.gs-combine-btn {
margin: 10px 16px 14px;
padding: 12px;
background: linear-gradient(#6b4b2a, #3c2414);
border: 2px solid #8b6a3c;
border-radius: 8px;
color: #f0d9a6;
font-family: "Cinzel", serif;
font-size: 14px;
font-weight: bold;
letter-spacing: 1px;
cursor: pointer;
transition: 0.2s;
flex-shrink: 0;
}
.gs-combine-btn:hover:not(:disabled) {
border-color: #f0d060;
color: #fff;
box-shadow: 0 0 12px rgba(200,160,60,0.4);
}
.gs-combine-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
/* ── Empty / Loading ─────────────────────── */
.gs-empty {
grid-column: 1 / -1; text-align: center;
color: #8b6a3c; font-size: 13px; padding: 30px 0;
}
.gs-loading {
text-align: center; color: #8b6a3c; font-size: 13px;
padding: 20px 0; animation: gs-pulse 1.2s infinite;
}
@keyframes gs-pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
</style>
`;
}
/* ══════════════════════════════════════════════
FRAKTIONS-TABS
══════════════════════════════════════════════ */
function renderTabs(groups) {
const container = document.getElementById("gs-faction-tabs");
if (!container) return;
container.innerHTML = groups
.map((g, i) => {
const color = g.color || "#6b4b2a";
return `
<button class="gs-tab ${i === 0 ? "gs-tab-active" : ""}"
data-group="${g.id}"
style="border-color:${color};${i === 0 ? `background:${color}33;` : ""}">
<span class="gs-tab-dot" style="background:${color};"></span>
<span>${g.name}</span>
</button>`;
})
.join("");
container.querySelectorAll(".gs-tab").forEach((btn) => {
btn.addEventListener("click", async () => {
const color = btn.style.borderColor;
container.querySelectorAll(".gs-tab").forEach((b) => {
b.classList.remove("gs-tab-active");
b.style.background = "";
});
btn.classList.add("gs-tab-active");
btn.style.background = `${color}33`;
gs_groupId = parseInt(btn.dataset.group);
gs_page = 1;
await loadCards();
});
});
}
/* ══════════════════════════════════════════════
KARTEN LADEN
══════════════════════════════════════════════ */
async function loadCards() {
const grid = document.getElementById("gs-grid");
const pagination = document.getElementById("gs-pagination");
if (!grid) return;
grid.innerHTML = `<div class="gs-loading">Lade Karten...</div>`;
if (pagination) pagination.innerHTML = "";
try {
const res = await fetch(
`/api/user-cards?group_id=${gs_groupId}&page=${gs_page}&limit=${CARDS_PER_PAGE}`
);
if (!res.ok) throw new Error();
const data = await res.json();
gs_cards = data.cards || [];
renderGrid(grid, gs_cards);
renderPagination(pagination, data.totalPages, data.total);
} catch {
grid.innerHTML = `<div class="gs-empty">Keine Karten gefunden.</div>`;
}
}
/* ══════════════════════════════════════════════
KARTEN-GRID RENDERN
══════════════════════════════════════════════ */
function renderGrid(grid, cards) {
if (!cards || !cards.length) {
grid.innerHTML = `<div class="gs-empty">Keine Karten in dieser Gruppe.</div>`;
return;
}
grid.innerHTML = cards.map((c) => {
// Karte deaktivieren wenn sie nicht zur aktuellen Kombination passt
const disabled = gs_combineId !== null && c.card_id !== gs_combineId;
// Anzahl bereits im Kombinier-Feld
const inField = gs_combineCards.filter(x => x.card_id === c.card_id).length;
const available = c.amount - inField;
return `
<div class="gs-card ${disabled ? "gs-card-disabled" : ""} ${gs_combineId === c.card_id ? "gs-card-selected" : ""}"
data-card-id="${c.card_id}"
title="${c.name}${disabled ? " (andere Karte bereits gewählt)" : ""}${available <= 0 ? " (alle im Feld)" : ""}">
<img src="/images/cards/${c.image}" alt="${c.name}"
onerror="this.src='/images/avatar_placeholder.svg'">
${c.attack != null ? `<span class="gs-stat-atk">${c.attack}</span>` : ""}
${c.defends != null ? `<span class="gs-stat-def">${c.defends}</span>` : ""}
<div class="gs-card-footer">
<span class="gs-count-owned">× ${available < 0 ? 0 : available}</span>
</div>
${c.rarity ? `<div class="gs-rarity">${rarityImgs(c.rarity, 12)}</div>` : ""}
</div>`;
}).join("");
grid.querySelectorAll(".gs-card:not(.gs-card-disabled)").forEach((el) => {
el.addEventListener("click", () => {
const card = cards.find((c) => c.card_id === parseInt(el.dataset.cardId));
if (!card) return;
const inField = gs_combineCards.filter(x => x.card_id === card.card_id).length;
if (inField >= card.amount) {
showToast("Keine weiteren Exemplare verfügbar.");
return;
}
addToField(card);
});
});
}
/* ══════════════════════════════════════════════
KARTE ZUM KOMBINIER-FELD HINZUFÜGEN
══════════════════════════════════════════════ */
function addToField(card) {
gs_combineId = card.card_id;
gs_combineCards.push({ ...card });
renderCombineField();
renderGrid(document.getElementById("gs-grid"), gs_cards);
}
/* ══════════════════════════════════════════════
KARTE AUS FELD ENTFERNEN
══════════════════════════════════════════════ */
function removeFromField(index) {
gs_combineCards.splice(index, 1);
if (gs_combineCards.length === 0) gs_combineId = null;
renderCombineField();
renderGrid(document.getElementById("gs-grid"), gs_cards);
}
/* ══════════════════════════════════════════════
KOMBINIER-FELD RENDERN
══════════════════════════════════════════════ */
function renderCombineField() {
const grid = document.getElementById("gs-combine-grid");
const hint = document.getElementById("gs-combine-hint");
const count = document.getElementById("gs-combine-count");
const btn = document.getElementById("gs-combine-btn");
if (!grid) return;
if (!gs_combineCards.length) {
grid.innerHTML = "";
hint.style.display = "block";
count.textContent = "";
btn.disabled = true;
return;
}
hint.style.display = "none";
const chance = getChance(gs_combineCards.length, gs_required);
const chanceColor = chance >= 100 ? "#44ff44" : chance >= 60 ? "#f0d060" : "#ff6644";
// Karten als gestapelter Stapel jede Karte leicht versetzt
const offset = Math.min(6, 40 / gs_combineCards.length); // max 6px versatz
const stackHeight = offset * (gs_combineCards.length - 1); // extra höhe für stapel
const stackCards = gs_combineCards.map((c, i) => `
<div class="gs-stack-card" style="
top: ${i * offset}px;
left: ${i * offset * 0.5}px;
z-index: ${i};
${i < gs_combineCards.length - 1 ? 'filter: brightness(0.75);' : ''}
">
<img src="/images/cards/${c.image}" alt="${c.name}"
onerror="this.src='/images/avatar_placeholder.svg'">
${c.cooldown != null ? `<span class="gs-stat-cd">${c.cooldown}</span>` : ""}
</div>
`).join("");
grid.innerHTML = `
<div class="gs-card-stack" style="margin-top:${stackHeight}px;" title="Klicken zum Entfernen">
${stackCards}
</div>`;
// Klick auf Stapel → oberste Karte entfernen
grid.querySelector(".gs-card-stack").addEventListener("click", () => {
removeFromField(gs_combineCards.length - 1);
});
count.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;gap:4px;">
<span>${gs_combineCards.length} Karte${gs_combineCards.length !== 1 ? "n" : ""} im Stapel</span>
<span style="font-size:15px;font-weight:bold;color:${chanceColor};">${chance}% Erfolgschance</span>
<div style="width:90%;height:6px;background:#2a1a08;border-radius:3px;border:1px solid #6b4b2a;">
<div style="width:${chance}%;height:100%;background:${chanceColor};border-radius:3px;transition:width 0.3s;"></div>
</div>
<span style="font-size:10px;color:#8b6a3c;">${gs_required} Karten = 100% (Gaststätte Lv.${gs_tavernLevel})</span>
</div>`;
btn.disabled = gs_combineCards.length < 2;
}
/* ══════════════════════════════════════════════
KOMBINIEREN-BUTTON
══════════════════════════════════════════════ */
function attachCombineEvents() {
// Wird nach renderShell aufgerufen
setTimeout(() => {
const btn = document.getElementById("gs-combine-btn");
if (!btn) return;
btn.addEventListener("click", async () => {
if (gs_combineCards.length < 2) return;
btn.disabled = true;
btn.textContent = "Kombiniere...";
try {
const res = await fetch("/api/cards/combine", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
card_id: gs_combineId,
amount: gs_combineCards.length,
}),
});
const data = await res.json();
if (!res.ok) {
showToast(data.error || "Kombinieren fehlgeschlagen.");
btn.disabled = false;
btn.textContent = "Kombinieren";
return;
}
if (!data.success) {
// Misserfolg Karten sind weg
showToast(`❌ Misserfolg! (${data.chance}% Chance) Karten verbraucht.`);
gs_combineCards = [];
gs_combineId = null;
renderCombineField();
await loadCards();
} else {
// Erfolg neue Karte zeigen
showToast(`✅ Erfolg! Du erhältst: ${data.reward.name} (Rarity ${data.reward.rarity})`);
gs_combineCards = [];
gs_combineId = null;
gs_initialized = false;
renderCombineField();
await loadCards();
}
} catch {
showToast("Verbindungsfehler.");
btn.disabled = false;
} finally {
btn.textContent = "Kombinieren";
}
});
}, 50);
}
/* ══════════════════════════════════════════════
PAGINATION
══════════════════════════════════════════════ */
function renderPagination(pagination, totalPages, total) {
if (!pagination || !totalPages || totalPages <= 1) return;
pagination.innerHTML = `
<button class="gs-page-btn" id="gs-prev" ${gs_page === 1 ? "disabled" : ""}>◀</button>
${Array.from({ length: totalPages }, (_, i) => i + 1)
.map((p) => `<button class="gs-page-btn ${p === gs_page ? "gs-page-active" : ""}" data-page="${p}">${p}</button>`)
.join("")}
<button class="gs-page-btn" id="gs-next" ${gs_page === totalPages ? "disabled" : ""}></button>
<span style="color:#a08060;font-size:11px;">${total} Karten</span>`;
pagination.querySelector("#gs-prev")?.addEventListener("click", async () => {
if (gs_page > 1) { gs_page--; await loadCards(); }
});
pagination.querySelector("#gs-next")?.addEventListener("click", async () => {
if (gs_page < totalPages) { gs_page++; await loadCards(); }
});
pagination.querySelectorAll("[data-page]").forEach((btn) => {
btn.addEventListener("click", async () => {
gs_page = parseInt(btn.dataset.page);
await loadCards();
});
});
}
/* ══════════════════════════════════════════════
TOAST
══════════════════════════════════════════════ */
function showToast(msg) {
const existing = document.querySelector(".gs-toast");
if (existing) existing.remove();
const t = document.createElement("div");
t.className = "gs-toast";
t.textContent = msg;
t.style.cssText = `
position:fixed;bottom:80px;left:50%;transform:translateX(-50%);
background:linear-gradient(#4a2808,#2a1004);border:2px solid #8b6a3c;
border-radius:8px;color:#f0d9a6;font-family:"Cinzel",serif;font-size:13px;
padding:10px 22px;z-index:9999;pointer-events:none;`;
document.body.appendChild(t);
setTimeout(() => t.remove(), 2800);
}