diff --git a/app.js b/app.js
index 7a47c6a..34ea516 100644
--- a/app.js
+++ b/app.js
@@ -25,12 +25,14 @@ const mineRoute = require("./routes/mine.route");
const carddeckRoutes = require("./routes/carddeck.route");
const arenaRoutes = require("./routes/arena.route");
const { registerArenaHandlers } = require("./sockets/arena.socket");
+const { registerHimmelstorHandlers } = require("./sockets/1vKI_daily.socket");
const { registerChatHandlers } = require("./sockets/chat");
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 himmelstorDailyRoutes = require("./routes/himmelstor-daily.route");
const compression = require("compression");
@@ -408,6 +410,7 @@ app.use("/api/points", pointsRoutes);
app.use("/api", combineRoutes);
app.use("/api", bazaarRoutes);
app.use("/himmelstor", himmelstorRoutes);
+app.use("/api/himmelstor/daily", himmelstorDailyRoutes);
/* ========================
404 Handler
@@ -425,6 +428,7 @@ io.on("connection", (socket) => {
console.log("Spieler verbunden:", socket.id);
registerChatHandlers(io, socket);
registerArenaHandlers(io, socket);
+ registerHimmelstorHandlers(io, socket);
});
/* ========================
diff --git a/public/css/daily.css b/public/css/daily.css
new file mode 100644
index 0000000..c3e41c0
--- /dev/null
+++ b/public/css/daily.css
@@ -0,0 +1,198 @@
+/* ============================================================
+ public/css/daily.css
+ Daily Herausforderung – Karten-Pfad Overlay
+============================================================ */
+
+/* ── Overlay ─────────────────────────────────────────────── */
+#daily-map-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 10000;
+ background: rgba(0, 0, 0, 0.92);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: dailyFadeIn 0.35s ease;
+}
+
+@keyframes dailyFadeIn { from { opacity: 0; } to { opacity: 1; } }
+@keyframes dailyPulse { 0%, 100% { box-shadow: 0 0 14px 4px rgba(80,160,255,0.7); } 50% { box-shadow: 0 0 28px 10px rgba(80,160,255,1); } }
+@keyframes dailyGlow { 0%, 100% { opacity: 1; } 50% { opacity: 0.65; } }
+
+/* ── Map Container ───────────────────────────────────────── */
+#daily-map-wrap {
+ position: relative;
+ max-width: min(95vw, 1000px);
+ max-height: 90vh;
+ border-radius: 14px;
+ overflow: hidden;
+ box-shadow: 0 0 0 2px rgba(80, 140, 255, 0.4), 0 30px 80px rgba(0, 0, 0, 0.9);
+}
+
+#daily-map-img {
+ display: block;
+ width: 100%;
+ height: auto;
+ max-height: 90vh;
+ object-fit: cover;
+ user-select: none;
+ pointer-events: none;
+}
+
+/* ── Close Button ────────────────────────────────────────── */
+#daily-map-close {
+ position: absolute;
+ top: 12px;
+ right: 14px;
+ background: rgba(0, 0, 0, 0.7);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ width: 34px;
+ height: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-size: 16px;
+ cursor: pointer;
+ z-index: 10;
+ transition: background 0.2s;
+}
+#daily-map-close:hover { background: rgba(200, 50, 50, 0.8); }
+
+/* ── Title Banner ────────────────────────────────────────── */
+#daily-map-title {
+ position: absolute;
+ top: 14px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: rgba(0, 0, 0, 0.72);
+ border: 1px solid rgba(80, 140, 255, 0.45);
+ border-radius: 8px;
+ padding: 6px 22px;
+ font-family: "Cinzel", serif;
+ font-size: 15px;
+ color: #a0c8ff;
+ letter-spacing: 3px;
+ white-space: nowrap;
+ z-index: 10;
+}
+
+/* ── Circles ─────────────────────────────────────────────── */
+.daily-circle {
+ position: absolute;
+ width: 7%; /* scales with image width */
+ aspect-ratio: 1;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: "Cinzel", serif;
+ font-size: clamp(12px, 1.8vw, 22px);
+ font-weight: bold;
+ transform: translate(-50%, -50%);
+ cursor: default;
+ transition: transform 0.15s ease;
+ z-index: 5;
+ user-select: none;
+}
+
+/* Completed station ─ grün */
+.daily-circle.done {
+ background: radial-gradient(circle at 40% 35%, #4ecf6a, #1a7a30);
+ border: 3px solid #7af09a;
+ color: #fff;
+ box-shadow: 0 0 14px rgba(78, 207, 106, 0.6);
+}
+.daily-circle.done::after {
+ content: "✓";
+ font-size: clamp(10px, 1.5vw, 18px);
+}
+
+/* Available station ─ blaues Pulsieren */
+.daily-circle.available {
+ background: radial-gradient(circle at 40% 35%, #5090ff, #1a3a9a);
+ border: 3px solid #90c0ff;
+ color: #fff;
+ cursor: pointer;
+ animation: dailyPulse 2s ease-in-out infinite;
+}
+.daily-circle.available:hover {
+ transform: translate(-50%, -50%) scale(1.15);
+ animation: none;
+ box-shadow: 0 0 30px 8px rgba(80, 160, 255, 0.9);
+}
+
+/* Locked station ─ ausgegraut */
+.daily-circle.locked {
+ background: radial-gradient(circle at 40% 35%, #2a2a3a, #141420);
+ border: 3px solid rgba(80, 100, 150, 0.4);
+ color: rgba(150, 160, 200, 0.35);
+}
+
+/* ── Progress Text ───────────────────────────────────────── */
+#daily-progress-bar-wrap {
+ position: absolute;
+ bottom: 14px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 60%;
+ background: rgba(0, 0, 0, 0.72);
+ border: 1px solid rgba(80, 140, 255, 0.35);
+ border-radius: 8px;
+ padding: 8px 16px;
+ z-index: 10;
+ text-align: center;
+ font-family: "Cinzel", serif;
+}
+
+#daily-progress-text {
+ font-size: 12px;
+ color: #a0c8ff;
+ letter-spacing: 1px;
+ margin-bottom: 5px;
+}
+
+#daily-progress-track {
+ height: 6px;
+ background: rgba(80, 140, 255, 0.15);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+#daily-progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #3060cc, #60a0ff);
+ border-radius: 3px;
+ transition: width 0.6s ease;
+}
+
+/* ── All Done ────────────────────────────────────────────── */
+#daily-all-done {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.78);
+ border-radius: 14px;
+ z-index: 20;
+ animation: dailyFadeIn 0.4s ease;
+}
+#daily-all-done .done-icon { font-size: 56px; margin-bottom: 14px; }
+#daily-all-done .done-title { font-family: "Cinzel", serif; font-size: 22px; color: #7af09a; letter-spacing: 4px; margin-bottom: 8px; }
+#daily-all-done .done-sub { font-family: "Cinzel", serif; font-size: 12px; color: #a0c8a0; margin-bottom: 24px; }
+#daily-all-done .done-btn {
+ background: linear-gradient(135deg, #1a4a28, #27ae60);
+ border: 2px solid rgba(100, 220, 100, 0.6);
+ border-radius: 10px;
+ color: #fff;
+ font-family: "Cinzel", serif;
+ font-size: 13px;
+ letter-spacing: 3px;
+ padding: 11px 32px;
+ cursor: pointer;
+ transition: 0.2s;
+}
+#daily-all-done .done-btn:hover { border-color: #7af09a; background: linear-gradient(135deg, #2a6a38, #37be70); }
diff --git a/public/images/KI_arena/weekly.jpeg b/public/images/KI_arena/daily.jpeg
similarity index 100%
rename from public/images/KI_arena/weekly.jpeg
rename to public/images/KI_arena/daily.jpeg
diff --git a/public/js/buildings/daily.js b/public/js/buildings/daily.js
new file mode 100644
index 0000000..5747a38
--- /dev/null
+++ b/public/js/buildings/daily.js
@@ -0,0 +1,211 @@
+/* ============================================================
+ public/js/buildings/daily.js
+ Tagesherausforderung – Karten-Pfad mit 7 Stationen gegen KI
+============================================================ */
+
+/* ── Stationen: Position auf dem Bild (% left, % top) ───── */
+const DAILY_STATIONS = [
+ { id: 1, left: 49.5, top: 86.5, label: "1" },
+ { id: 2, left: 37.0, top: 73.0, label: "2" },
+ { id: 3, left: 44.5, top: 62.0, label: "3" },
+ { id: 4, left: 54.5, top: 53.5, label: "4" },
+ { id: 5, left: 49.0, top: 46.0, label: "5" },
+ { id: 6, left: 44.5, top: 37.5, label: "6" },
+ { id: 7, left: 50.0, top: 22.0, label: "7" },
+];
+
+let dailyDeckId = null;
+let dailyOverlay = null;
+
+/* ── Öffnet den Karten-Pfad ──────────────────────────────── */
+export async function openDailyMap(deckId) {
+ dailyDeckId = deckId;
+
+ // Kein Duplikat
+ document.getElementById("daily-map-overlay")?.remove();
+
+ /* Fortschritt vom Server laden */
+ let completed = [];
+ try {
+ const res = await fetch("/api/himmelstor/daily/progress");
+ if (res.ok) {
+ const data = await res.json();
+ completed = data.completed ?? [];
+ }
+ } catch (e) { console.error("[Daily] Fortschritt laden:", e); }
+
+ /* Overlay aufbauen */
+ dailyOverlay = document.createElement("div");
+ dailyOverlay.id = "daily-map-overlay";
+ dailyOverlay.innerHTML = `
+
+

+
☀️ TAGESHERAUSFORDERUNG
+
+ ${buildCircles(completed)}
+
+
${completed.length} / 7 Stationen abgeschlossen
+
+
+ ${completed.length >= 7 ? buildAllDonePanel() : ""}
+
`;
+
+ document.body.appendChild(dailyOverlay);
+
+ /* Event-Listener */
+ document.getElementById("daily-map-close").addEventListener("click", closeDailyMap);
+ dailyOverlay.addEventListener("click", e => { if (e.target === dailyOverlay) closeDailyMap(); });
+
+ /* Klickbare Kreise */
+ dailyOverlay.querySelectorAll(".daily-circle.available").forEach(circle => {
+ circle.addEventListener("click", () => {
+ const station = parseInt(circle.dataset.station, 10);
+ startDailyStation(station);
+ });
+ });
+}
+
+/* ── HTML der Kreise ─────────────────────────────────────── */
+function buildCircles(completed) {
+ const maxDone = completed.length; // nächster freier = maxDone + 1
+
+ return DAILY_STATIONS.map(s => {
+ const isDone = completed.includes(s.id);
+ const isAvailable = !isDone && s.id === maxDone + 1;
+ const isLocked = !isDone && !isAvailable;
+
+ const cls = isDone ? "done" : isAvailable ? "available" : "locked";
+ const inner = isDone ? "" : s.label; // done zeigt ✓ per CSS ::after
+
+ return `${inner}
`;
+ }).join("");
+}
+
+function buildAllDonePanel() {
+ return `
+
+
🏆
+
TAGESQUEST ABGESCHLOSSEN!
+
Du hast alle 7 Stationen gemeistert.
+
+
`;
+}
+
+/* ── Station starten ─────────────────────────────────────── */
+function startDailyStation(station) {
+ if (!dailyDeckId) {
+ alert("Bitte zuerst ein Deck auswählen.");
+ return;
+ }
+
+ const socket = window._socket;
+ if (!socket) {
+ console.error("[Daily] Kein Socket.");
+ return;
+ }
+
+ /* Kreis als "lädt" markieren */
+ const circle = dailyOverlay.querySelector(`.daily-circle[data-station="${station}"]`);
+ if (circle) {
+ circle.style.animation = "none";
+ circle.style.opacity = "0.5";
+ circle.style.cursor = "wait";
+ }
+
+ /* Einmaligen Listener für Match-Gefunden */
+ socket.once("ht_daily_match_found", data => {
+ closeDailyMap();
+ openHtDailyPopup(
+ `/himmelstor/daily?match=${encodeURIComponent(data.matchId)}&slot=${encodeURIComponent(data.mySlot)}&deck=${encodeURIComponent(dailyDeckId)}&station=${station}&opponent=${encodeURIComponent("Wächter " + station)}`,
+ "Wächter " + station,
+ data.matchId
+ );
+ });
+
+ socket.once("ht_daily_error", data => {
+ if (circle) { circle.style.animation = ""; circle.style.opacity = ""; circle.style.cursor = ""; }
+ alert(data.message || "Fehler beim Starten.");
+ });
+
+ socket.emit("ht_join_daily", {
+ station,
+ deckId: dailyDeckId,
+ });
+}
+
+/* ── Popup öffnen (Iframe wie bei Arena) ─────────────────── */
+function openHtDailyPopup(src, opponentName, matchId) {
+ document.getElementById("ht-daily-backdrop")?.remove();
+ document.getElementById("ht-daily-popup")?.remove();
+
+ const backdrop = document.createElement("div");
+ backdrop.id = "ht-daily-backdrop";
+ backdrop.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,.82);backdrop-filter:blur(5px);z-index:9998;";
+
+ const popup = document.createElement("div");
+ popup.id = "ht-daily-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(80,140,255,.4),0 30px 90px rgba(0,0,0,.85);";
+ popup.innerHTML = `
+
+ ☀️ Daily · vs ${opponentName}
+ ${matchId}
+
+ `;
+
+ document.body.appendChild(backdrop);
+ document.body.appendChild(popup);
+
+ /* Cleanup wenn Iframe-Spiel beendet → closeToArena ruft parent auf */
+}
+
+/* ── Overlay schließen ───────────────────────────────────── */
+function closeDailyMap() {
+ document.getElementById("daily-map-overlay")?.remove();
+ dailyOverlay = null;
+}
+
+/* ── Extern: nach gewonnenem Match Kreis aktualisieren ───── */
+export function markDailyStationDone(station) {
+ if (!dailyOverlay) return;
+ const circle = dailyOverlay.querySelector(`.daily-circle[data-station="${station}"]`);
+ if (!circle) return;
+ circle.className = "daily-circle done";
+ circle.textContent = "";
+ circle.style = "";
+
+ /* Nächste Station freischalten */
+ const next = dailyOverlay.querySelector(`.daily-circle[data-station="${station + 1}"]`);
+ if (next) {
+ next.className = "daily-circle available";
+ next.textContent = String(station + 1);
+ next.addEventListener("click", () => startDailyStation(station + 1));
+ }
+
+ /* Fortschrittsbalken updaten */
+ const done = dailyOverlay.querySelectorAll(".daily-circle.done").length;
+ const pct = Math.round((done / 7) * 100);
+ const fill = document.getElementById("daily-progress-fill");
+ const text = document.getElementById("daily-progress-text");
+ if (fill) fill.style.width = pct + "%";
+ if (text) text.textContent = `${done} / 7 Stationen abgeschlossen`;
+
+ /* Alle 7 fertig? */
+ if (done >= 7) {
+ const wrap = document.getElementById("daily-map-wrap");
+ if (wrap && !document.getElementById("daily-all-done")) {
+ const panel = document.createElement("div");
+ panel.innerHTML = buildAllDonePanel();
+ wrap.appendChild(panel.firstElementChild);
+ }
+ }
+}
diff --git a/public/js/buildings/himmelstor.js b/public/js/buildings/himmelstor.js
index d40d67e..7123616 100644
--- a/public/js/buildings/himmelstor.js
+++ b/public/js/buildings/himmelstor.js
@@ -1,3 +1,4 @@
+import { openDailyMap, markDailyStationDone } from './daily.js';
/* ============================================================
public/js/buildings/himmelstor.js
Himmelstor – Tages- und Wochenherausforderung
@@ -244,11 +245,29 @@ function initHimmelstorModes() {
htSelectedDeckId = select ? Number(select.value) : null;
if (!htSelectedDeckId) return;
sessionStorage.setItem("selectedDeckId", htSelectedDeckId);
+ if (mode === 'daily') {
+ // Karten-Pfad öffnen statt direktes Matchmaking
+ openDailyMap(htSelectedDeckId);
+ return;
+ }
handleHtModeClick(card, mode);
});
});
}
+/* ── Station-Complete Event vom Server ──────────────────── */
+function setupHtSocketListeners() {
+ const socket = window._socket;
+ if (!socket) return;
+ socket.off('ht_station_complete');
+ socket.on('ht_station_complete', data => {
+ markDailyStationDone(data.station);
+ });
+}
+// Listener einrichten sobald Socket verfügbar
+if (window._socket) setupHtSocketListeners();
+else document.addEventListener('socket_ready', setupHtSocketListeners);
+
/* ── Modus klicken ───────────────────────────────────────── */
async function handleHtModeClick(card, mode) {
if (card.classList.contains("searching")) return;
diff --git a/routes/himmelstor-daily.route.js b/routes/himmelstor-daily.route.js
new file mode 100644
index 0000000..d440aa6
--- /dev/null
+++ b/routes/himmelstor-daily.route.js
@@ -0,0 +1,31 @@
+/* ============================================================
+ routes/himmelstor-daily.route.js
+ GET /api/himmelstor/daily/progress – welche Stationen heute erledigt
+============================================================ */
+
+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();
+}
+
+/* ── GET /api/himmelstor/daily/progress ────────────────── */
+router.get("/progress", requireLogin, async (req, res) => {
+ const userId = req.session.user.id;
+ try {
+ const [rows] = await db.query(
+ "SELECT event_id FROM daily_completions WHERE user_id = ?",
+ [userId]
+ );
+ const completed = rows.map(r => r.event_id);
+ res.json({ completed, total: 7, allDone: completed.length >= 7 });
+ } catch (err) {
+ console.error("[Daily] Fortschritt laden:", err);
+ res.status(500).json({ error: "DB Fehler" });
+ }
+});
+
+module.exports = router;
diff --git a/sockets/1vKI_daily.socket.js b/sockets/1vKI_daily.socket.js
new file mode 100644
index 0000000..b3e4178
--- /dev/null
+++ b/sockets/1vKI_daily.socket.js
@@ -0,0 +1,483 @@
+/* ============================================================
+ sockets/1vKI_daily.socket.js
+ KI-Matches für die Tagesherausforderung (Himmelstor Daily)
+ – Haiku entscheidet den KI-Zug via Anthropic API
+============================================================ */
+
+'use strict';
+
+const db = require('../database/database');
+const { runCombatPhase } = require('./combat');
+const pointsRoute = require('../routes/points.route');
+
+/* ── Set aller aktiven KI-Match-IDs (wird von arena.socket.js importiert) */
+const htAiMatchIds = new Set();
+
+/* ── KI-Match Rooms ─────────────────────────────────────────
+ matchId → {
+ playerSocketId, playerSlot, playerAccountId,
+ aiSlot, aiHand, aiDeck,
+ boardState, boardCards,
+ leftSlot, hp, maxHp, station,
+ gameOver, aiTurnInProgress
+ }
+*/
+const htAiRooms = new Map();
+
+/* ── HP-Formel (identisch zu arena.socket.js) ─────────────── */
+function calcAvatarHp(level) {
+ return 20 + (Math.max(1, Math.min(50, level || 1)) - 1) * 2;
+}
+
+/* ── Wächter-Namen pro Station ────────────────────────────── */
+const GUARD_NAMES = [
+ "", "Torwächter", "Waldläufer", "Steinbrecher",
+ "Schattenjäger", "Eisenherzog", "Sturmritter", "Himmelsherold",
+];
+
+/* ── KI-Deck laden (Schwierigkeit nach Station) ───────────── */
+async function loadAiDeck(station) {
+ const maxRarity = station <= 2 ? 1 : station <= 4 ? 2 : station <= 6 ? 3 : 4;
+ try {
+ const [cards] = await db.query(
+ `SELECT id, name, image, attack, defends, cooldown, \`range\`, \`race\`, rarity
+ FROM cards WHERE rarity <= ? ORDER BY RAND() LIMIT 10`,
+ [maxRarity]
+ );
+ if (cards.length === 0) {
+ // Fallback: alle Karten
+ const [all] = await db.query(
+ `SELECT id, name, image, attack, defends, cooldown, \`range\`, \`race\`, rarity
+ FROM cards ORDER BY RAND() LIMIT 10`
+ );
+ return all.map(c => ({ ...c, currentCd: 0 }));
+ }
+ return cards.map(c => ({ ...c, currentCd: 0 }));
+ } catch (err) {
+ console.error('[HT] loadAiDeck Fehler:', err);
+ return [];
+ }
+}
+
+/* ── boardState → boardCards Array ───────────────────────── */
+function boardStateToCards(boardState) {
+ return Object.entries(boardState).map(([boardSlot, entry]) => ({
+ boardSlot, card: entry.card, owner: entry.owner, slot: entry.owner,
+ }));
+}
+
+/* ── Haiku API: KI-Zug entscheiden ───────────────────────── */
+async function askHaiku(boardState, aiHand, availableSlots, aiSlot, leftSlot) {
+ const apiKey = process.env.ANTHROPIC_API_KEY;
+ if (!apiKey) { console.warn('[HT] ANTHROPIC_API_KEY fehlt'); return null; }
+
+ const isLeft = aiSlot === leftSlot;
+
+ // Vereinfachter Board-Überblick
+ const boardSummary = Object.entries(boardState).map(([slot, e]) => ({
+ slot, card: e.card.name, owner: e.owner === aiSlot ? 'KI' : 'Spieler',
+ atk: e.card.attack, def: e.card.defends,
+ }));
+
+ const handSummary = aiHand.map((c, i) => ({
+ index: i, name: c.name, attack: c.attack ?? 0,
+ defends: c.defends ?? 0, race: c.race ?? 0, range: c.range ?? 1,
+ }));
+
+ const prompt = `Kartenspiel. Du bist die KI (${isLeft ? 'linke Seite, Slots 1-3, bewegst dich nach rechts' : 'rechte Seite, Slots 9-11, bewegst dich nach links'}).
+Board: ${JSON.stringify(boardSummary)}
+Deine Hand: ${JSON.stringify(handSummary)}
+Freie Slots: ${JSON.stringify(availableSlots)}
+
+Wähle eine Karte und einen freien Slot. Bevorzuge Karten mit hohem Angriff oder hoher Bewegung (race).
+Antworte NUR mit validem JSON: {"card_index":0,"slot":"row1-slot-9"}
+Wenn keine Karte oder kein Slot: {"skip":true}`;
+
+ try {
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-api-key': apiKey,
+ 'anthropic-version': '2023-06-01',
+ },
+ body: JSON.stringify({
+ model: 'claude-haiku-4-5-20251001',
+ max_tokens: 80,
+ messages: [{ role: 'user', content: prompt }],
+ }),
+ });
+ const data = await res.json();
+ const text = data.content?.[0]?.text ?? '{}';
+ // JSON aus Antwort extrahieren
+ const match = text.match(/\{[^}]+\}/);
+ if (!match) return null;
+ return JSON.parse(match[0]);
+ } catch (err) {
+ console.error('[HT] Haiku API Fehler:', err);
+ return null;
+ }
+}
+
+/* ── KI-Zug ausführen ─────────────────────────────────────── */
+async function playAiTurn(io, matchId) {
+ const aiRoom = htAiRooms.get(matchId);
+ if (!aiRoom || aiRoom.aiTurnInProgress || aiRoom.gameOver) return;
+ aiRoom.aiTurnInProgress = true;
+
+ try {
+ // Freie Slots auf KI-Seite
+ const isLeft = aiRoom.aiSlot === aiRoom.leftSlot;
+ const aiIdxs = isLeft ? [1, 2, 3] : [9, 10, 11];
+ const freeSlots = [];
+ for (const row of ['row1', 'row2']) {
+ for (const idx of aiIdxs) {
+ const slotId = `${row}-slot-${idx}`;
+ if (!aiRoom.boardState[slotId]) freeSlots.push(slotId);
+ }
+ }
+
+ const readyCards = aiRoom.aiHand.filter(c => (c.currentCd ?? 0) <= 0);
+
+ if (readyCards.length > 0 && freeSlots.length > 0) {
+ const decision = await askHaiku(
+ aiRoom.boardState, readyCards, freeSlots, aiRoom.aiSlot, aiRoom.leftSlot
+ );
+
+ if (decision && !decision.skip && decision.card_index != null && decision.slot) {
+ const card = readyCards[decision.card_index];
+ const slotId = decision.slot;
+
+ if (card && freeSlots.includes(slotId)) {
+ // Karte aufs Board legen
+ aiRoom.boardState[slotId] = { card, owner: aiRoom.aiSlot };
+ aiRoom.boardCards = boardStateToCards(aiRoom.boardState);
+
+ // Karte aus Hand entfernen, neue Karte ziehen
+ const idx = aiRoom.aiHand.findIndex(c => c === card);
+ if (idx > -1) aiRoom.aiHand.splice(idx, 1);
+ if (aiRoom.aiDeck.length > 0) {
+ const drawn = aiRoom.aiDeck.shift();
+ drawn.currentCd = drawn.cooldown ?? 0;
+ aiRoom.aiHand.push(drawn);
+ }
+
+ // Spieler informieren
+ io.to(aiRoom.playerSocketId).emit('card_played', {
+ matchId, slot: aiRoom.aiSlot, boardSlot: slotId,
+ row: slotId.split('-slot-')[0],
+ slotIndex: parseInt(slotId.split('-slot-')[1]),
+ card,
+ });
+
+ console.log(`[HT] KI spielt: ${card.name} → ${slotId} | Match ${matchId}`);
+ await sleep(900); // Kurze Pause damit Spieler die Karte sieht
+ }
+ }
+ }
+
+ // KI-Hand Cooldowns ticken
+ aiRoom.aiHand.forEach(c => { if ((c.currentCd ?? 0) > 0) c.currentCd--; });
+
+ // Kampfphase – KI ist am Zug
+ const combatEvents = runCombatPhase(aiRoom.boardState, aiRoom.leftSlot, aiRoom.aiSlot);
+ aiRoom.boardCards = boardStateToCards(aiRoom.boardState);
+ const finalBoard = aiRoom.boardCards;
+
+ io.to(aiRoom.playerSocketId).emit('combat_phase', { events: combatEvents, finalBoard });
+
+ // Avatar-Schaden verarbeiten
+ const matchEnded = await htProcessAvatarAttacks(io, matchId, aiRoom, combatEvents);
+ if (matchEnded) return;
+
+ // Zug zurück an Spieler
+ const delay = calcCombatDuration(combatEvents) + 300;
+ setTimeout(() => {
+ if (aiRoom.gameOver) return;
+ io.to(aiRoom.playerSocketId).emit('turn_change', {
+ activeSlot: aiRoom.playerSlot,
+ boardSync: aiRoom.boardCards,
+ hp: aiRoom.hp,
+ maxHp: aiRoom.maxHp,
+ });
+ console.log(`[HT] Zug zurück an Spieler | Match ${matchId}`);
+ }, delay);
+
+ } finally {
+ aiRoom.aiTurnInProgress = false;
+ }
+}
+
+/* ── Avatar-Schaden (KI-Match) ────────────────────────────── */
+async function htProcessAvatarAttacks(io, matchId, aiRoom, events) {
+ const avatarEvents = events.filter(e => e.type === 'avatar_attack');
+ if (avatarEvents.length === 0) return false;
+
+ for (const ev of avatarEvents) {
+ const target = ev.target;
+ if (aiRoom.hp[target] == null) continue;
+
+ aiRoom.hp[target] = Math.max(0, aiRoom.hp[target] - (ev.damage ?? 0));
+
+ io.to(aiRoom.playerSocketId).emit('avatar_damaged', {
+ slot: target, damage: ev.damage,
+ remainingHp: aiRoom.hp[target], maxHp: aiRoom.maxHp[target] ?? 20,
+ });
+
+ if (aiRoom.hp[target] <= 0) {
+ await htHandleMatchEnd(io, matchId, aiRoom, target);
+ return true;
+ }
+ }
+ return false;
+}
+
+/* ── Match beenden (KI-Match) ──────────────────────────────── */
+async function htHandleMatchEnd(io, matchId, aiRoom, loserSlot) {
+ if (aiRoom.gameOver) return;
+ aiRoom.gameOver = true;
+ htAiMatchIds.delete(matchId);
+
+ const playerWon = loserSlot === aiRoom.aiSlot;
+ console.log(`[HT] Match Ende: ${playerWon ? 'Spieler gewinnt' : 'KI gewinnt'} | Station ${aiRoom.station} | ${matchId}`);
+
+ if (playerWon) {
+ // Station als abgeschlossen markieren
+ try {
+ await db.query(
+ `INSERT IGNORE INTO daily_completions (user_id, event_id) VALUES (?, ?)`,
+ [aiRoom.playerAccountId, aiRoom.station]
+ );
+ } catch (err) {
+ console.error('[HT] Completion speichern Fehler:', err);
+ }
+
+ // Punkte vergeben (Platzhalter: 10 Punkte pro Station)
+ let winResult = { awarded: 0 };
+ if (aiRoom.playerAccountId) {
+ try {
+ winResult = await pointsRoute.awardPoints(aiRoom.playerAccountId, 10);
+ } catch {}
+ }
+
+ io.to(aiRoom.playerSocketId).emit('ht_station_complete', {
+ station: aiRoom.station,
+ });
+
+ io.to(aiRoom.playerSocketId).emit('match_result', {
+ won: true,
+ awarded: winResult.awarded ?? 0,
+ level_up: winResult.level_up ?? false,
+ new_level:winResult.new_level ?? null,
+ });
+ } else {
+ io.to(aiRoom.playerSocketId).emit('match_result', {
+ won: false,
+ awarded: 0,
+ });
+ }
+
+ // Aufräumen nach kurzer Verzögerung
+ setTimeout(() => {
+ htAiRooms.delete(matchId);
+ }, 30000);
+}
+
+/* ── Animation-Dauer Berechnung ──────────────────────────── */
+function calcCombatDuration(events) {
+ let total = 600 + 500 + 400;
+ for (const ev of events) {
+ if (ev.type === 'move') total += 350;
+ if (ev.type === 'attack') total += 450;
+ if (ev.type === 'die') total += 300;
+ if (ev.type === 'avatar_attack') total += 500;
+ }
+ return total;
+}
+
+function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
+
+/* ═══════════════════════════════════════════════════════════
+ HAUPT-HANDLER
+═══════════════════════════════════════════════════════════ */
+function registerHimmelstorHandlers(io, socket) {
+
+ /* ── Spieler betritt Daily-Match ────────────────────────── */
+ socket.on('ht_join_daily', async (data) => {
+ const { station, deckId } = data;
+ if (!station || station < 1 || station > 7) return;
+
+ // Spieler-Account laden
+ let accountId = null, playerLevel = 1;
+ if (socket.request?.session?.user?.id) {
+ accountId = socket.request.session.user.id;
+ try {
+ const [[acc]] = await db.query(
+ "SELECT level FROM accounts WHERE id = ?", [accountId]
+ );
+ playerLevel = acc?.level ?? 1;
+ } catch {}
+ }
+
+ // Prüfen ob Station bereits abgeschlossen
+ if (accountId) {
+ const [done] = await db.query(
+ "SELECT id FROM daily_completions WHERE user_id = ? AND event_id = ?",
+ [accountId, station]
+ );
+ if (done.length > 0) {
+ socket.emit('ht_daily_error', { message: `Station ${station} wurde heute bereits abgeschlossen.` });
+ return;
+ }
+ }
+
+ // KI-Deck laden
+ const aiDeck = await loadAiDeck(station);
+ if (aiDeck.length === 0) {
+ socket.emit('ht_daily_error', { message: 'Keine Karten für diese Station gefunden.' });
+ return;
+ }
+
+ // Match erstellen
+ const matchId = `ht_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
+ const playerSlot = 'player1'; // Spieler immer links
+ const aiSlot = 'player2';
+ const leftSlot = 'player1';
+
+ const maxHpPlayer = calcAvatarHp(playerLevel);
+ const maxHpAi = calcAvatarHp(Math.min(50, station * 7)); // KI wird mit Station stärker
+
+ // AI-Room anlegen
+ const aiRoom = {
+ playerSocketId : socket.id,
+ playerSlot,
+ playerAccountId : accountId,
+ aiSlot,
+ aiHand : aiDeck.slice(0, 3), // 3 Startkarten
+ aiDeck : aiDeck.slice(3),
+ boardState : {},
+ boardCards : [],
+ leftSlot,
+ hp : { player1: maxHpPlayer, player2: maxHpAi },
+ maxHp : { player1: maxHpPlayer, player2: maxHpAi },
+ station,
+ gameOver : false,
+ aiTurnInProgress: false,
+ };
+ htAiRooms.set(matchId, aiRoom);
+ htAiMatchIds.add(matchId);
+
+ // io._arenaRooms eintragen damit arena_join/battlefield funktioniert
+ if (!io._arenaRooms) io._arenaRooms = new Map();
+ io._arenaRooms.set(matchId, {
+ sockets : { player1: socket.id, player2: null }, // player2 = KI (kein socket)
+ names : { player1: 'Du', player2: GUARD_NAMES[station] || `Wächter ${station}` },
+ accountIds : { player1: accountId, player2: null },
+ boardCards : [],
+ boardState : aiRoom.boardState, // Referenz! Gleicher Zustand
+ leftSlot,
+ hp : aiRoom.hp,
+ maxHp : aiRoom.maxHp,
+ gameOver : false,
+ });
+
+ console.log(`[HT] Daily Match: Station ${station} | ${matchId} | Spieler ${socket.id}`);
+
+ // Match-Gefunden an Spieler senden
+ socket.emit('ht_daily_match_found', {
+ matchId,
+ mySlot: playerSlot,
+ });
+ });
+
+ /* ── arena_join für KI-Matches abfangen ─────────────────── */
+ socket.on('arena_join', (data) => {
+ const { matchId } = data;
+ if (!matchId || !htAiMatchIds.has(matchId)) return; // Nur KI-Matches hier
+
+ const aiRoom = htAiRooms.get(matchId);
+ if (!aiRoom) return;
+
+ // Socket aktualisieren (nach Reconnect)
+ aiRoom.playerSocketId = socket.id;
+ const ioRoom = io._arenaRooms?.get(matchId);
+ if (ioRoom) ioRoom.sockets.player1 = socket.id;
+
+ socket.join('arena_' + matchId);
+
+ // Direkt arena_ready senden (kein warten auf Gegner)
+ socket.emit('arena_ready', {
+ player1 : 'Du',
+ player2 : GUARD_NAMES[aiRoom.station] || `Wächter ${aiRoom.station}`,
+ boardSync: [],
+ hp : aiRoom.hp,
+ maxHp : aiRoom.maxHp,
+ });
+
+ // HP initialisieren
+ socket.emit('hp_init', { hp: aiRoom.hp, maxHp: aiRoom.maxHp });
+
+ // Spieler braucht "bereit" nicht – direkt Zug starten
+ // (Wir simulieren ready_status so dass der Client den leftSlot setzt)
+ setTimeout(() => {
+ socket.emit('ready_status', {
+ readyCount: 2,
+ readySlots: ['player1', 'player2'],
+ });
+ }, 200);
+
+ setTimeout(() => {
+ socket.emit('turn_change', {
+ activeSlot: aiRoom.playerSlot, // Spieler fängt an
+ boardSync : [],
+ hp : aiRoom.hp,
+ maxHp : aiRoom.maxHp,
+ });
+ }, 600);
+
+ console.log(`[HT] arena_join KI-Match: ${matchId}`);
+ });
+
+ /* ── end_turn für KI-Matches ────────────────────────────── */
+ socket.on('end_turn', async (data) => {
+ const { matchId, slot } = data;
+ if (!matchId || !htAiMatchIds.has(matchId)) return; // Nur KI-Matches hier
+
+ const aiRoom = htAiRooms.get(matchId);
+ if (!aiRoom || aiRoom.gameOver) return;
+ if (slot !== aiRoom.playerSlot) return; // Nur Spieler-Züge akzeptieren
+
+ // boardState aus io._arenaRooms synchronisieren (Spieler hat Karten gelegt)
+ const ioRoom = io._arenaRooms?.get(matchId);
+ if (ioRoom) {
+ aiRoom.boardState = ioRoom.boardState;
+ aiRoom.boardCards = boardStateToCards(aiRoom.boardState);
+ }
+
+ // Kampfphase – Spieler ist am Zug
+ const combatEvents = runCombatPhase(aiRoom.boardState, aiRoom.leftSlot, aiRoom.playerSlot);
+ aiRoom.boardCards = boardStateToCards(aiRoom.boardState);
+ if (ioRoom) { ioRoom.boardState = aiRoom.boardState; ioRoom.boardCards = aiRoom.boardCards; }
+
+ socket.emit('combat_phase', { events: combatEvents, finalBoard: aiRoom.boardCards });
+
+ // Avatar-Schaden nach Spieler-Zug prüfen
+ const matchEnded = await htProcessAvatarAttacks(io, matchId, aiRoom, combatEvents);
+ if (matchEnded) return;
+
+ // Nach Animationsdauer → KI-Zug
+ const delay = calcCombatDuration(combatEvents) + 400;
+ setTimeout(async () => {
+ if (aiRoom.gameOver) return;
+ await playAiTurn(io, matchId);
+ }, delay);
+ });
+
+ /* ── station_complete bestätigen (nach iframe-close) ────── */
+ socket.on('ht_station_ack', (data) => {
+ // Kann verwendet werden um UI zu aktualisieren
+ console.log(`[HT] Station ACK: ${data.station}`);
+ });
+}
+
+module.exports = { registerHimmelstorHandlers, htAiMatchIds };
diff --git a/sockets/arena.socket.js b/sockets/arena.socket.js
index fba52d6..3c29f31 100644
--- a/sockets/arena.socket.js
+++ b/sockets/arena.socket.js
@@ -5,6 +5,7 @@
============================================================ */
const { runCombatPhase } = require('./combat');
+const { htAiMatchIds } = require('./1vKI_daily.socket');
const db = require('../database/database');
const pointsRoute = require('../routes/points.route');
@@ -596,6 +597,7 @@ function registerArenaHandlers(io, socket) {
console.warn(`[1v1] arena_join abgewiesen – matchId oder slot fehlt`);
return;
}
+ if (htAiMatchIds.has(matchId)) return; // wird von himmelstor.socket.js behandelt
if (!io._arenaRooms) io._arenaRooms = new Map();
if (!io._arenaRooms.has(matchId)) {
@@ -742,6 +744,7 @@ function registerArenaHandlers(io, socket) {
socket.on("end_turn", async (data) => {
const { matchId, slot } = data;
if (!matchId || !slot) return;
+ if (htAiMatchIds.has(matchId)) return; // wird von himmelstor.socket.js behandelt
const room = io._arenaRooms?.get(matchId);
if (!room) return;
diff --git a/views/launcher.ejs b/views/launcher.ejs
index b714e5c..91e0dad 100644
--- a/views/launcher.ejs
+++ b/views/launcher.ejs
@@ -94,6 +94,7 @@
+