dok/public/js/gaststaette/glucksspiel.js
2026-04-11 13:37:00 +01:00

447 lines
18 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);
}
function gsRangeIcon() {
return `<svg viewBox="0 0 16 16" width="11" height="11" 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>`;
}
function gsRaceIcon() {
return `<svg viewBox="0 0 16 16" width="11" height="11" 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>`;
}
// 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;
// CSS einmalig laden
if (!document.querySelector('link[href="/css/glucksspiel.css"]')) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "/css/glucksspiel.css";
document.head.appendChild(link);
}
// 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>
`;
}
/* ══════════════════════════════════════════════
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>` : ""}
${c.cooldown != null ? `<span class="gs-stat-cd">${c.cooldown}</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>` : ""}
${(c.range != null || c.race != null) ? `
<div class="gs-range-race">
${c.range != null ? `<span class="gs-badge-range">${gsRangeIcon()}&thinsp;${c.range}</span>` : ""}
${c.race != null ? `<span class="gs-badge-race">${gsRaceIcon()}&thinsp;${c.race}</span>` : ""}
</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.attack != null ? `<span class="gs-stat-atk">${c.attack}</span>` : ""}
${c.defends != null ? `<span class="gs-stat-def">${c.defends}</span>` : ""}
${c.cooldown != null ? `<span class="gs-stat-cd">${c.cooldown}</span>` : ""}
${c.rarity ? `<div class="gs-rarity">${rarityImgs(c.rarity, 12)}</div>` : ""}
${(c.range != null || c.race != null) ? `
<div class="gs-range-race">
${c.range != null ? `<span class="gs-badge-range">${gsRangeIcon()}&thinsp;${c.range}</span>` : ""}
${c.race != null ? `<span class="gs-badge-race">${gsRaceIcon()}&thinsp;${c.race}</span>` : ""}
</div>` : ""}
</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);
}