From f535f1f83ced6525862430e7ebf6d3ae600b63a5 Mon Sep 17 00:00:00 2001 From: cay Date: Tue, 14 Apr 2026 07:50:14 +0100 Subject: [PATCH] gj,gd --- app.js | 2 + public/css/himmelstor.css | 120 ++++++++++ public/js/buildings/himmelstor.js | 382 ++++++++++++++++++++++++++++++ public/js/map-ui.js | 4 +- routes/himmelstor.route.js | 112 +++++++++ views/launcher.ejs | 57 +++-- 6 files changed, 662 insertions(+), 15 deletions(-) create mode 100644 public/css/himmelstor.css create mode 100644 public/js/buildings/himmelstor.js create mode 100644 routes/himmelstor.route.js diff --git a/app.js b/app.js index 552ed11..7a47c6a 100644 --- a/app.js +++ b/app.js @@ -30,6 +30,7 @@ const boosterRoutes = require("./routes/booster.route"); const pointsRoutes = require("./routes/points.route"); const combineRoutes = require("./routes/combine.route"); const bazaarRoutes = require("./routes/bazaar.route"); +const himmelstorRoutes = require("./routes/himmelstor.route"); const compression = require("compression"); @@ -406,6 +407,7 @@ app.use("/api", require("./routes/daily.route")); app.use("/api/points", pointsRoutes); app.use("/api", combineRoutes); app.use("/api", bazaarRoutes); +app.use("/himmelstor", himmelstorRoutes); /* ======================== 404 Handler diff --git a/public/css/himmelstor.css b/public/css/himmelstor.css new file mode 100644 index 0000000..bf4e89a --- /dev/null +++ b/public/css/himmelstor.css @@ -0,0 +1,120 @@ +/* ================================ + HIMMELSTOR UI + public/css/himmelstor.css +================================ */ + +#himmelstor-ui { + display: flex; + flex-direction: column; + align-items: center; + padding: 32px 20px; + min-height: 100%; +} + +.ht-title { + font-size: 28px; + font-weight: bold; + color: #c8a0ff; + text-shadow: + 0 0 8px rgba(155, 114, 207, 0.8), + 2px 2px 4px rgba(0, 0, 0, 0.9); + letter-spacing: 3px; + text-transform: uppercase; + margin-bottom: 6px; + font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif; +} + +.ht-subtitle { + color: #7a6090; + font-size: 13px; + margin: 0 0 36px 0; + font-style: italic; + letter-spacing: 1px; +} + +/* ================================ + MODE CARDS +================================ */ + +.ht-modes { + display: flex; + gap: 24px; + flex-wrap: wrap; + justify-content: center; +} + +.ht-mode-card { + width: 160px; + background: linear-gradient( + 160deg, + rgba(25, 15, 40, 0.95) 0%, + rgba(10, 8, 20, 0.98) 100% + ); + border: 2px solid #4a3a6b; + border-radius: 6px; + padding: 28px 16px 22px; + text-align: center; + cursor: pointer; + position: relative; + overflow: hidden; + box-shadow: + inset 0 1px 0 rgba(155, 114, 207, 0.15), + 0 4px 16px rgba(0, 0, 0, 0.6); + transition: + transform 0.15s ease, + border-color 0.15s ease, + box-shadow 0.15s ease; +} + +/* Ornamental top line */ +.ht-mode-card::before { + content: ""; + position: absolute; + top: 0; + left: 10%; + right: 10%; + height: 2px; + background: linear-gradient( + 90deg, + transparent, + #9b72cf, + transparent + ); +} + +.ht-mode-card:hover { + transform: translateY(-4px); + border-color: #9b72cf; + box-shadow: + inset 0 1px 0 rgba(155, 114, 207, 0.25), + 0 8px 24px rgba(0, 0, 0, 0.7), + 0 0 12px rgba(155, 114, 207, 0.25); +} + +.ht-mode-card:active { + transform: translateY(-1px); +} + +.ht-mode-icon { + font-size: 36px; + margin-bottom: 12px; + line-height: 1; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8)); +} + +.ht-mode-label { + font-size: 22px; + font-weight: bold; + color: #c8a0ff; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.9); + letter-spacing: 2px; + font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif; + margin-bottom: 10px; +} + +.ht-mode-desc { + font-size: 11px; + color: #6a5080; + line-height: 1.5; + font-style: italic; +} diff --git a/public/js/buildings/himmelstor.js b/public/js/buildings/himmelstor.js new file mode 100644 index 0000000..d40d67e --- /dev/null +++ b/public/js/buildings/himmelstor.js @@ -0,0 +1,382 @@ +/* ============================================================ + 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 + ? `` + : `` + + decks.filter(d => d.card_count > 0) + .map(d => ``) + .join("") + + (decks.some(d => d.card_count === 0) + ? decks.filter(d => d.card_count === 0) + .map(d => ``) + .join("") + : ""); + + ui.innerHTML = ` +
+
+
🌤️ Himmelstor
+

Wähle Deck und Herausforderung

+ +
+ +
+
+
☀️
+
Daily
+
Tagesherausforderung
+
+ +
+ +
+
+
🌙
+
Weekly
+
Wochenherausforderung
+
+ +
+ +
+ + ${decks.length === 0 + ? `
⚠️ Kein Deck vorhanden – gehe zur Burg → Kartendeck
` + : ""} + + +
+
`; + + 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} +
Suche abbrechen`; + 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 = ` +
🌤️ Herausforderung!
+
${myName}  vs  ${opponentName}
+
`; + 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 = ` +
+ 🌤️ Himmelstor · vs ${opponentName} + ${matchId || src} +
+ `; + document.body.appendChild(backdrop); + document.body.appendChild(popup); +} diff --git a/public/js/map-ui.js b/public/js/map-ui.js index 13cdcae..3fb4833 100644 --- a/public/js/map-ui.js +++ b/public/js/map-ui.js @@ -1,4 +1,5 @@ import { loadArena } from "./buildings/arena.js?v=4"; +import { loadHimmelstor } from "./buildings/himmelstor.js"; import { loadCharacterHouse } from "./buildings/character-house.js?v=2"; import { loadBlackmarket } from "./buildings/blackmarket.js?v=2"; import { loadMine } from "./buildings/mine.js?v=2"; @@ -48,7 +49,8 @@ document.getElementById("qm-overlay")?.addEventListener("mouseenter", () => { }); const buildingModules = { - 1: loadArena, // Arena – eigenes UI, Tabs ausblenden + 1: loadArena, // Arena – eigenes UI, Tabs ausblenden + 3: loadHimmelstor, // Himmelstor – eigenes UI, Tabs ausblenden 10: loadMine, // Mine – Tabs bleiben sichtbar 11: loadCharacterHouse, // Charakterhaus – eigenes UI, Tabs ausblenden 12: loadBlackmarket, // Schwarzmarkt – eigenes UI, Tabs ausblenden diff --git a/routes/himmelstor.route.js b/routes/himmelstor.route.js new file mode 100644 index 0000000..b58491a --- /dev/null +++ b/routes/himmelstor.route.js @@ -0,0 +1,112 @@ +/* ============================================================ + routes/himmelstor.route.js + GET /himmelstor/daily – Tagesherausforderung Spielfeld + GET /himmelstor/weekly – Wochenherausforderung Spielfeld +============================================================ */ + +const express = require("express"); +const router = express.Router(); +const db = require("../database/database"); + +function requireLogin(req, res, next) { + if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" }); + next(); +} + +/* HP-Formel: 20 + (level-1)*2 */ +function calcAvatarHp(level) { + return 20 + (Math.max(1, Math.min(50, level || 1)) - 1) * 2; +} + +async function getPlayerHp(userId) { + try { + const [[acc]] = await db.query("SELECT level FROM accounts WHERE id = ?", [userId]); + return calcAvatarHp(acc?.level ?? 1); + } catch { + return 20; + } +} + +/* ── GET /himmelstor/daily ─────────────────────────────── */ +router.get("/daily", requireLogin, async (req, res) => { + const userId = req.session.user.id; + const { match: matchId, slot } = req.query; + + if (!matchId) { + return res.render("1v1-battlefield", { + title: "☀️ Daily Herausforderung", + matchId: null, + mySlot: "player1", + player1: req.session?.user?.ingame_name || "Spieler 1", + player2: "Gegner", + player1hp: 20, + player1mana: 3, + player2hp: 20, + player2mana: 3, + }); + } + + try { + const [[me]] = await db.query("SELECT ingame_name FROM accounts WHERE id = ?", [userId]); + const hp = await getPlayerHp(userId); + const isP1 = slot === "player1"; + + res.render("1v1-battlefield", { + title: "☀️ Daily Herausforderung", + matchId, + mySlot: slot || "player1", + player1: isP1 ? (me?.ingame_name || "Du") : "Gegner", + player2: isP1 ? "Gegner" : (me?.ingame_name || "Du"), + player1hp: isP1 ? hp : 20, + player1mana: 3, + player2hp: isP1 ? 20 : hp, + player2mana: 3, + }); + } catch (err) { + console.error("[Himmelstor /daily]", err); + res.status(500).send("Fehler beim Laden des Spielfelds."); + } +}); + +/* ── GET /himmelstor/weekly ────────────────────────────── */ +router.get("/weekly", requireLogin, async (req, res) => { + const userId = req.session.user.id; + const { match: matchId, slot } = req.query; + + if (!matchId) { + return res.render("1v1-battlefield", { + title: "🌙 Weekly Herausforderung", + matchId: null, + mySlot: "player1", + player1: req.session?.user?.ingame_name || "Spieler 1", + player2: "Gegner", + player1hp: 20, + player1mana: 3, + player2hp: 20, + player2mana: 3, + }); + } + + try { + const [[me]] = await db.query("SELECT ingame_name FROM accounts WHERE id = ?", [userId]); + const hp = await getPlayerHp(userId); + const isP1 = slot === "player1"; + + res.render("1v1-battlefield", { + title: "🌙 Weekly Herausforderung", + matchId, + mySlot: slot || "player1", + player1: isP1 ? (me?.ingame_name || "Du") : "Gegner", + player2: isP1 ? "Gegner" : (me?.ingame_name || "Du"), + player1hp: isP1 ? hp : 20, + player1mana: 3, + player2hp: isP1 ? 20 : hp, + player2mana: 3, + }); + } catch (err) { + console.error("[Himmelstor /weekly]", err); + res.status(500).send("Fehler beim Laden des Spielfelds."); + } +}); + +module.exports = router; diff --git a/views/launcher.ejs b/views/launcher.ejs index b065a51..b714e5c 100644 --- a/views/launcher.ejs +++ b/views/launcher.ejs @@ -93,9 +93,13 @@ + - + @@ -656,12 +660,16 @@
- + 0
- + 0
@@ -669,12 +677,16 @@
- + 0
- + 0
@@ -796,15 +808,25 @@
- +
-

Inhalt folgt...

-

Inhalt folgt...

+
+

+ Inhalt folgt... +

+
+
+

+ Inhalt folgt... +

+
@@ -898,7 +920,9 @@ 💠 Gems kaufen
-
Unterstütze das Spiel und erhalte wertvolle Gems!
+
+ Unterstütze das Spiel und erhalte wertvolle Gems! +