From a31431ecaff0c3c4344fa66ee76e0eff285df6a2 Mon Sep 17 00:00:00 2001 From: cay Date: Tue, 14 Apr 2026 08:31:15 +0100 Subject: [PATCH] tdkdtz --- app.js | 4 + public/css/daily.css | 198 +++++++ .../KI_arena/{weekly.jpeg => daily.jpeg} | Bin public/js/buildings/daily.js | 211 ++++++++ public/js/buildings/himmelstor.js | 19 + routes/himmelstor-daily.route.js | 31 ++ sockets/1vKI_daily.socket.js | 483 ++++++++++++++++++ sockets/arena.socket.js | 3 + views/launcher.ejs | 1 + 9 files changed, 950 insertions(+) create mode 100644 public/css/daily.css rename public/images/KI_arena/{weekly.jpeg => daily.jpeg} (100%) create mode 100644 public/js/buildings/daily.js create mode 100644 routes/himmelstor-daily.route.js create mode 100644 sockets/1vKI_daily.socket.js 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 = ` +
+ Daily +
☀️ 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 @@ +