From a85ac02f2530bff3f807cc03bc2e24069c8eb38c Mon Sep 17 00:00:00 2001 From: cay Date: Mon, 13 Apr 2026 17:21:54 +0100 Subject: [PATCH] gc,lhgtck --- public/js/buildings/1v1.js | 190 ++++++++++++++++++++++++++++++++++++- sockets/arena.socket.js | 168 +++++++++++++++++++++----------- sockets/combat.js | 182 +++++++++++++++++++++++++++++++++++ 3 files changed, 481 insertions(+), 59 deletions(-) create mode 100644 sockets/combat.js diff --git a/public/js/buildings/1v1.js b/public/js/buildings/1v1.js index 6511a5c..0860163 100644 --- a/public/js/buildings/1v1.js +++ b/public/js/buildings/1v1.js @@ -1,4 +1,4 @@ -/* ═══════════════════════════════════════════════════════════════ +/* ═══════════════════════════════════════════════════════════════ public/js/buildings/1v1.js Vollstaendige Spielfeld-Logik fuer das 1v1-Battlefield. EJS-Variablen kommen aus window.GAME_CONFIG (in der EJS gesetzt). @@ -281,6 +281,12 @@ document.getElementById('end-turn-btn')?.addEventListener('click', endMyTurn); /* ═══════════════════════════════════════════════════════════ KARTE AUF BOARD RENDERN ═══════════════════════════════════════════════════════════ */ + +/* + * boardState speichert jetzt: { [slotId]: { card, owner } } + * card = Karten-Objekt mit aktuellen Werten (defends wird live verändert) + * owner = 'player1' | 'player2' + */ const boardState = {}; function buildStatsHtml(card) { @@ -314,6 +320,15 @@ function renderCardInSlot(slot, card) { : `
\u2694\uFE0F${card.name}
${buildStatsHtml(card)}`; } +/* Slot leeren (nach Tod oder Bewegung) */ +function clearBoardSlot(slotId) { + delete boardState[slotId]; + const el = document.getElementById(slotId); + if (!el) return; + el.classList.remove('slot-occupied'); + el.innerHTML = '\u2736' + slotId.split('-slot-')[1] + ''; +} + /* ═══════════════════════════════════════════════════════════ SOCKET & ARENA-JOIN @@ -356,12 +371,17 @@ const readyFallbackTimer = setTimeout(() => { }, 10000); /* ── Board-Sync ──────────────────────────────────────────── */ +/* + * boardSync-Einträge müssen vom Server nun { boardSlot, card, owner } enthalten. + * Ältere Sync-Daten ohne owner werden als gegnerisch behandelt (Fallback). + */ function applyBoardSync(cards) { if (!cards || !cards.length) return; cards.forEach(cd => { const slotEl = document.getElementById(cd.boardSlot); if (!slotEl || boardState[cd.boardSlot]) return; - boardState[cd.boardSlot] = cd.card; + const owner = cd.owner ?? (cd.slot ?? 'unknown'); + boardState[cd.boardSlot] = { card: cd.card, owner }; renderCardOnBoard(slotEl, cd.card); }); console.log('[1v1] Board sync:', cards.length, 'Karten'); @@ -565,7 +585,9 @@ document.getElementById('handArea').addEventListener('dragend', e => { handSlotState[sourceId] = null; renderHandSlot(sourceId); - boardState[slot.id] = cardState.card; + + /* ── boardState mit owner speichern ─────────────────── */ + boardState[slot.id] = { card: cardState.card, owner: mySlot }; renderCardOnBoard(slot, cardState.card); slot.classList.remove('drop-zone-active', 'drop-zone-hover'); @@ -582,11 +604,171 @@ socket.on('card_played', data => { if (boardState[data.boardSlot]) return; const slotEl = document.getElementById(data.boardSlot); if (!slotEl) { console.warn('[1v1] card_played: Slot fehlt:', data.boardSlot); return; } - boardState[data.boardSlot] = data.card; + + /* ── boardState mit owner des Gegners speichern ─────── */ + boardState[data.boardSlot] = { card: data.card, owner: data.slot }; renderCardOnBoard(slotEl, data.card); console.log('[1v1] Gegner Karte:', data.card?.name, '->', data.boardSlot); }); + +/* ═══════════════════════════════════════════════════════════ + KAMPFPHASE – CLIENT-SEITIGE VERARBEITUNG + Der Server sendet nach end_turn ein 'combat_phase'-Event mit: + { events: [...], finalBoard: [...] } +═══════════════════════════════════════════════════════════ */ + +/* Timing (ms) zwischen einzelnen Kampf-Events */ +const COMBAT_DELAY_MOVE = 350; +const COMBAT_DELAY_ATTACK = 450; +const COMBAT_DELAY_DIE = 300; + +/* Kampf-Log-Banner (erscheint kurz über dem Board) */ +function showCombatBanner(text, color = '#f0d060') { + const old = document.getElementById('combat-banner'); + if (old) old.remove(); + const el = document.createElement('div'); + el.id = 'combat-banner'; + el.style.cssText = ` + position:fixed;top:18%;left:50%;transform:translateX(-50%); + background:rgba(10,8,5,0.92);border:1px solid ${color}; + border-radius:8px;padding:7px 22px; + color:${color};font-family:'Cinzel',serif;font-size:12px; + letter-spacing:2px;z-index:500;pointer-events:none; + animation:combat-fade 0.25s ease;`; + el.textContent = text; + document.body.appendChild(el); + setTimeout(() => el.remove(), 1600); +} + +/* Kurzes Aufleuchten eines Slots (Angriff / Tod) */ +function flashSlot(slotId, color, durationMs = 300) { + const el = document.getElementById(slotId); + if (!el) return; + el.style.transition = `box-shadow ${durationMs / 2}ms ease`; + el.style.boxShadow = `0 0 18px 6px ${color}`; + setTimeout(() => { el.style.boxShadow = ''; }, durationMs); +} + +/* Karte bewegen: Slot A → Slot B */ +function applyMoveEvent(ev) { + const entry = boardState[ev.from]; + if (!entry) return; + + const fromEl = document.getElementById(ev.from); + const toEl = document.getElementById(ev.to); + if (!fromEl || !toEl) return; + + /* boardState aktualisieren */ + delete boardState[ev.from]; + boardState[ev.to] = entry; + + /* DOM aktualisieren */ + fromEl.classList.remove('slot-occupied'); + fromEl.innerHTML = '\u2736' + ev.from.split('-slot-')[1] + ''; + + renderCardOnBoard(toEl, entry.card); + + console.log(`[Combat] Bewegt: ${entry.card.name} ${ev.from} → ${ev.to}`); +} + +/* Angriff: Verteidigung des Ziels aktualisieren */ +function applyAttackEvent(ev) { + const attackerEntry = boardState[ev.from]; + const targetEntry = boardState[ev.to]; + if (!attackerEntry || !targetEntry) return; + + /* Verteidigung im boardState aktualisieren */ + targetEntry.card = { ...targetEntry.card, defends: ev.remainingDef }; + + /* Visuelles Feedback */ + flashSlot(ev.from, 'rgba(255,200,50,0.7)', 250); // Angreifer leuchtet gold + flashSlot(ev.to, 'rgba(220,50,50,0.85)', 350); // Ziel leuchtet rot + + /* Verteidigungswert im DOM sofort aktualisieren */ + const targetEl = document.getElementById(ev.to); + if (targetEl) { + const defEl = targetEl.querySelector('.cs-def'); + if (defEl) { + defEl.textContent = ev.remainingDef; + defEl.style.color = ev.remainingDef <= 2 ? '#e74c3c' : ''; + } + } + + console.log(`[Combat] Angriff: ${attackerEntry.card.name} → ${targetEntry.card.name} (${ev.damage} Schaden, verbleibend: ${ev.remainingDef})`); +} + +/* Karte stirbt */ +function applyDieEvent(ev) { + const entry = boardState[ev.slotId]; + const name = entry?.card?.name ?? '???'; + + flashSlot(ev.slotId, 'rgba(200,50,50,0.9)', 500); + + setTimeout(() => { + clearBoardSlot(ev.slotId); + console.log(`[Combat] Gestorben: ${name} auf ${ev.slotId}`); + }, 200); +} + +/* Finales Board nach Kampfphase vollständig anwenden (Sicherheits-Sync) */ +function applyFinalBoard(finalBoard) { + /* Erst alles leeren */ + Object.keys(boardState).forEach(sid => clearBoardSlot(sid)); + + /* Dann neuen Zustand setzen */ + if (!finalBoard || !finalBoard.length) return; + finalBoard.forEach(entry => { + const slotEl = document.getElementById(entry.boardSlot); + if (!slotEl) return; + boardState[entry.boardSlot] = { card: entry.card, owner: entry.owner }; + renderCardOnBoard(slotEl, entry.card); + }); +} + +/* Haupt-Handler für combat_phase */ +socket.on('combat_phase', data => { + const events = data.events ?? []; + const finalBoard = data.finalBoard ?? []; + + console.log(`[Combat] Kampfphase startet: ${events.length} Events`); + showCombatBanner('\u2694\uFE0F KAMPFPHASE'); + + if (events.length === 0) { + /* Keine Karten im Spiel → direkt final sync */ + applyFinalBoard(finalBoard); + return; + } + + /* Events sequenziell abarbeiten */ + let delay = 600; // kurze Pause nach Banner + + events.forEach(ev => { + const thisDelay = delay; + + if (ev.type === 'move') { + delay += COMBAT_DELAY_MOVE; + setTimeout(() => applyMoveEvent(ev), thisDelay); + + } else if (ev.type === 'attack') { + delay += COMBAT_DELAY_ATTACK; + setTimeout(() => applyAttackEvent(ev), thisDelay); + + } else if (ev.type === 'die') { + delay += COMBAT_DELAY_DIE; + setTimeout(() => applyDieEvent(ev), thisDelay); + } + }); + + /* Nach allen Events → finaler Sync (stellt sicher dass alles stimmt) */ + delay += 500; + setTimeout(() => { + applyFinalBoard(finalBoard); + console.log('[Combat] Kampfphase abgeschlossen, Board synchronisiert.'); + }, delay); +}); + + /* ═══════════════════════════════════════════════════════════ AUFGEBEN ═══════════════════════════════════════════════════════════ */ diff --git a/sockets/arena.socket.js b/sockets/arena.socket.js index eea576b..e3e8011 100644 --- a/sockets/arena.socket.js +++ b/sockets/arena.socket.js @@ -1,9 +1,11 @@ /* ============================================================ - sockets/arena.js + sockets/arena.socket.js 1v1 Matchmaking + 2v2 Team-Lobby + 4v4 Team-Lobby - inkl. Kick-Funktion für Team-Leader wurde getestet + inkl. Kampfphase nach end_turn ============================================================ */ +const { runCombatPhase } = require('./combat'); // combat.js liegt im selben /sockets/ Ordner + const waitingPool = new Map(); const LEVEL_RANGE = 5; const READY_TIMEOUT = 30; @@ -56,9 +58,9 @@ function broadcastTeamStatus(io, teamId, mode) { if (!team) return; const data = { teamId, - leaderId: team.leaderId, // ← Leader-SocketId mitschicken + leaderId: team.leaderId, players: team.players.map((p) => ({ - socketId: p.socketId, // ← für Kick-Button im Frontend + socketId: p.socketId, name: p.player.name, level: p.player.level, ready: team.ready.has(p.socketId), @@ -133,7 +135,6 @@ function leaveAllTeams_nvn(socketId, io, mode) { teams.delete(teamId); console.log(`[${mode}] Team ${teamId} aufgelöst.`); } else { - // Falls Leader das Team verlässt → nächsten Spieler zum Leader machen if (team.leaderId === socketId) { team.leaderId = team.players[0].socketId; console.log( @@ -216,6 +217,39 @@ function stopReadyTimer(io, matchId) { } } +/* ═══════════════════════════════════════════════════════════ + KAMPFPHASE HELPER + Berechnet wie lange die Client-Animation dauert, + damit turn_change erst NACH den Animationen gesendet wird. +═══════════════════════════════════════════════════════════ */ +function calcCombatDuration(events) { + // Muss mit den COMBAT_DELAY_* Werten in 1v1.js übereinstimmen + const DELAY_BANNER = 600; // initiale Pause vor erstem Event + const DELAY_MOVE = 350; + const DELAY_ATTACK = 450; + const DELAY_DIE = 300; + const DELAY_FINAL = 500; // finalBoard sync + const BUFFER = 400; // Sicherheitspuffer + + let total = DELAY_BANNER + DELAY_FINAL + BUFFER; + for (const ev of events) { + if (ev.type === 'move') total += DELAY_MOVE; + if (ev.type === 'attack') total += DELAY_ATTACK; + if (ev.type === 'die') total += DELAY_DIE; + } + return total; +} + +/* ── boardState → boardCards-Array (für reconnect/boardSync) ── */ +function boardStateToCards(boardState) { + return Object.entries(boardState).map(([boardSlot, entry]) => ({ + boardSlot, + card : entry.card, + owner: entry.owner, + slot : entry.owner, // Rückwärtskompatibilität + })); +} + /* ═══════════════════════════════════════════════════════════ Handler-Generator für 2v2 und 4v4 ═══════════════════════════════════════════════════════════ */ @@ -253,7 +287,7 @@ function registerTeamModeHandlers(io, socket, mode) { getTeamMap(mode).set(teamId, { id: teamId, - leaderId: socket.id, // ← Ersteller wird Leader + leaderId: socket.id, players: [{ socketId: socket.id, player }], ready: new Set(), }); @@ -306,14 +340,12 @@ function registerTeamModeHandlers(io, socket, mode) { if (!team) return; - // Nur der Leader darf kicken if (team.leaderId !== socket.id) { return socket.emit(`${mode}_error`, { message: "Nur der Team-Leader kann Spieler entfernen.", }); } - // Sich selbst kicken nicht erlaubt if (targetSocketId === socket.id) return; const idx = team.players.findIndex((p) => p.socketId === targetSocketId); @@ -324,7 +356,6 @@ function registerTeamModeHandlers(io, socket, mode) { team.ready.delete(targetSocketId); getReadyMap(mode).delete(teamId); - // Gekickten Spieler benachrichtigen io.to(targetSocketId).emit(`${mode}_kicked`, { message: "Du wurdest vom Team-Leader aus dem Team entfernt.", }); @@ -364,7 +395,6 @@ function getRoom(io, matchId) { return io._arenaRooms?.get(matchId) || null; } -// Sendet an beide Spieler über ihre gespeicherten Socket-IDs function emitToMatch(io, matchId, event, data) { const room = getRoom(io, matchId); if (!room) { @@ -379,7 +409,6 @@ function emitToMatch(io, matchId, event, data) { }); } -// Sendet an den Gegner des Senders function emitToOpponent(io, matchId, senderSlot, event, data) { const room = getRoom(io, matchId); if (!room) return; @@ -428,22 +457,24 @@ function registerArenaHandlers(io, socket) { } if (!io._arenaRooms) io._arenaRooms = new Map(); - if (!io._arenaRooms.has(matchId)) - io._arenaRooms.set(matchId, { sockets: {}, names: {}, boardCards: [] }); + if (!io._arenaRooms.has(matchId)) { + io._arenaRooms.set(matchId, { + sockets : {}, + names : {}, + boardCards: [], // Array-Format für boardSync/reconnect + boardState: {}, // NEU: { [slotId]: { card, owner } } für Kampfphase + leftSlot : null, // NEU: welcher Slot ist der linke Spieler + }); + } const room = io._arenaRooms.get(matchId); room.sockets[slot] = socket.id; - // Name: direkt vom Client (kommt aus /arena/me), socket.user wird ignoriert - // da es oft Default-Werte enthält die den echten Namen überschreiben würden if (playerName && playerName !== "Spieler") { - // Guter Name vom Client → immer übernehmen room.names[slot] = playerName; } else if (!room.names[slot] || room.names[slot] === "Spieler") { - // Kein guter Name bekannt → Fallback room.names[slot] = playerName || "Spieler"; } - // Sonst: bereits guter Name gespeichert → nicht überschreiben console.log( `[1v1] Name gesetzt: slot=${slot}, name=${room.names[slot]}, playerName=${playerName}`, @@ -451,7 +482,6 @@ function registerArenaHandlers(io, socket) { socket.join("arena_" + matchId); - // Bereits gespielte Karten an neu verbundenen Spieler senden if (room.boardCards?.length > 0) { socket.emit("board_sync", { cards: room.boardCards }); } @@ -462,8 +492,8 @@ function registerArenaHandlers(io, socket) { `[1v1] Beide Spieler da → arena_ready senden | Match ${matchId}`, ); emitToMatch(io, matchId, "arena_ready", { - player1: room.names["player1"] || "Spieler 1", - player2: room.names["player2"] || "Spieler 2", + player1 : room.names["player1"] || "Spieler 1", + player2 : room.names["player2"] || "Spieler 2", boardSync: room.boardCards || [], }); startReadyTimer(io, matchId); @@ -496,8 +526,6 @@ function registerArenaHandlers(io, socket) { if (readySet.size >= 2) { stopReadyTimer(io, matchId); io._arenaReady.delete(matchId); - // Startspieler wird vom Client per start_turn_request gesendet - // (Client kennt durch seed-basiertes Flip wer links = wer anfängt) console.log( `[1v1] Beide bereit – warte auf start_turn_request | Match ${matchId}`, ); @@ -509,66 +537,96 @@ function registerArenaHandlers(io, socket) { io.to("arena_" + matchId).emit("player_surrendered", { slot }); }); - /* ── Zugwechsel ── */ - socket.on("end_turn", (data) => { - const { matchId, slot } = data; - if (!matchId || !slot) return; - const nextSlot = slot === "player1" ? "player2" : "player1"; - - // Board-State mitschicken → beide Clients können Board synchronisieren - const room = io._arenaRooms?.get(matchId); - const boardCards = room?.boardCards || []; - - emitToMatch(io, matchId, "turn_change", { - activeSlot: nextSlot, - boardSync: boardCards, - }); - - console.log( - `[1v1] Zug: ${slot} → ${nextSlot} | boardCards=${boardCards.length} | Match ${matchId}`, - ); - }); - - /* ── Karte gespielt → direkt an Gegner senden ── */ + /* ── Karte gespielt ── */ socket.on("card_played", (data) => { - const { matchId, slot } = data; + const { matchId, slot, boardSlot, card } = data; if (!matchId) return; - // Direkt an Gegner-Socket senden + // An Gegner senden emitToOpponent(io, matchId, slot, "card_played", data); - // Karte im Server-State speichern (für Reconnect/board_sync) - const roomState = io._arenaRooms?.get(matchId); - if (roomState) { - roomState.boardCards = roomState.boardCards || []; - roomState.boardCards.push(data); + // Im Server-State speichern + const room = io._arenaRooms?.get(matchId); + if (room) { + // boardState mit Owner speichern (für Kampfphase) + room.boardState[boardSlot] = { card, owner: slot }; + + // boardCards neu aufbauen (für Reconnect/boardSync) + room.boardCards = boardStateToCards(room.boardState); } console.log( - `[1v1] card_played: ${data.card?.name} → ${data.boardSlot} | Match ${matchId}`, + `[1v1] card_played: ${card?.name} → ${boardSlot} (owner: ${slot}) | Match ${matchId}`, ); }); - // Client sendet nach ready_status den leftSlot (wer anfängt) + /* ── Startspieler festlegen ── */ socket.on("start_turn_request", (data) => { const { matchId, starterSlot } = data; if (!matchId || !starterSlot) return; if (!io._turnInit) io._turnInit = new Set(); - // Nur einmal pro Match ausführen if (io._turnInit.has(matchId)) return; io._turnInit.add(matchId); setTimeout(() => io._turnInit?.delete(matchId), 60000); + + // NEU: linken Spieler im Room merken (wird für Kampfphase benötigt) const room = io._arenaRooms?.get(matchId); + if (room) room.leftSlot = starterSlot; + const boardCards = room?.boardCards || []; emitToMatch(io, matchId, "turn_change", { activeSlot: starterSlot, - boardSync: boardCards, + boardSync : boardCards, }); console.log( `[1v1] Spiel startet → ${starterSlot} (linker Spieler) beginnt | Match ${matchId}`, ); }); + /* ── Zug beenden → Kampfphase → Zugwechsel ── */ + socket.on("end_turn", (data) => { + const { matchId, slot } = data; + if (!matchId || !slot) return; + + const room = io._arenaRooms?.get(matchId); + if (!room) return; + + const leftSlot = room.leftSlot || 'player1'; + const boardState = room.boardState; + const nextSlot = slot === "player1" ? "player2" : "player1"; + + /* ── Kampfphase berechnen ── */ + const combatEvents = runCombatPhase(boardState, leftSlot); + + // boardCards nach Kampf aktualisieren (Karten die gestorben sind fehlen jetzt) + room.boardCards = boardStateToCards(boardState); + + // finalBoard: aktueller Stand nach Kampf + const finalBoard = room.boardCards; + + // Kampf-Events + finales Board an beide Clients senden + emitToMatch(io, matchId, 'combat_phase', { + events : combatEvents, + finalBoard: finalBoard, + }); + + console.log( + `[1v1] Kampfphase: ${combatEvents.length} Events | Zug: ${slot} → ${nextSlot} | Match ${matchId}`, + ); + + // Zugwechsel NACH den Client-Animationen senden + const animDuration = calcCombatDuration(combatEvents); + setTimeout(() => { + emitToMatch(io, matchId, "turn_change", { + activeSlot: nextSlot, + boardSync : room.boardCards, // aktualisierter Stand nach Kampf + }); + console.log( + `[1v1] turn_change gesendet nach ${animDuration}ms | ${slot} → ${nextSlot} | Match ${matchId}`, + ); + }, animDuration); + }); + /* ── 2v2 & 4v4 ── */ registerTeamModeHandlers(io, socket, "2v2"); registerTeamModeHandlers(io, socket, "4v4"); diff --git a/sockets/combat.js b/sockets/combat.js new file mode 100644 index 0000000..1ecaf66 --- /dev/null +++ b/sockets/combat.js @@ -0,0 +1,182 @@ +/** + * sockets/combat.js – Server-seitige Kampfphasen-Logik für 1v1 + * + * boardState-Format (server-seitig): + * { [slotId]: { card: { name, attack, defends, range, race, ... }, owner: 'player1'|'player2' } } + * + * Bewegungsrichtung: + * leftSlot-Spieler → dir = +1 (Slot 1 → 11) + * rightSlot-Spieler → dir = −1 (Slot 11 → 1) + * + * Verarbeitungs-Reihenfolge: + * Slot 11 → 1, pro Slot: row1 zuerst, dann row2 + * + * Jede Karte wird genau einmal verarbeitet (Snapshot der Startreihenfolge). + * Bewegung: race Schritte vorwärts, stoppt vor eigener UND feindlicher Karte. + * Angriff: scannt range Felder vorwärts, überspringt eigene Karten, + * greift die erste feindliche Karte an (nur eine pro Zug). + */ + +'use strict'; + +/** + * @param {Object} boardState – wird in-place verändert + * @param {string} leftSlot – 'player1' oder 'player2' (wer links steht) + * @returns {Array} – geordnete Event-Liste für den Client + */ +function runCombatPhase(boardState, leftSlot) { + const events = []; + + /* ── Reihenfolge einmalig snapshot-en ──────────────────────────── */ + const processingOrder = []; + for (let slotIndex = 11; slotIndex >= 1; slotIndex--) { + for (const row of ['row1', 'row2']) { + const slotId = `${row}-slot-${slotIndex}`; + if (boardState[slotId]) { + processingOrder.push(slotId); + } + } + } + + /* ── Jede Karte einzeln verarbeiten ────────────────────────────── */ + for (const startSlotId of processingOrder) { + const entry = boardState[startSlotId]; + if (!entry) continue; // wurde in dieser Runde bereits getötet + + const { card, owner } = entry; + const isLeft = owner === leftSlot; + const dir = isLeft ? 1 : -1; + const row = startSlotId.split('-slot-')[0]; // 'row1' oder 'row2' + + let currentPos = parseInt(startSlotId.split('-slot-')[1], 10); + let currentSlotId = startSlotId; + + /* ── BEWEGUNG (race) ──────────────────────────────────────────── */ + const race = card.race ?? 0; + + for (let step = 0; step < race; step++) { + const nextPos = currentPos + dir; + if (nextPos < 1 || nextPos > 11) break; + + const nextSlotId = `${row}-slot-${nextPos}`; + + // Blockiert durch eigene ODER feindliche Karte → stehen bleiben + if (boardState[nextSlotId]) break; + + // Slot frei → Karte verschieben + delete boardState[currentSlotId]; + boardState[nextSlotId] = entry; + + events.push({ + type : 'move', + from : currentSlotId, + to : nextSlotId, + owner, + }); + + currentSlotId = nextSlotId; + currentPos = nextPos; + } + + /* ── ANGRIFF (range) ──────────────────────────────────────────── */ + const range = card.range ?? 0; + + for (let r = 1; r <= range; r++) { + const targetPos = currentPos + dir * r; + if (targetPos < 1 || targetPos > 11) break; + + const targetSlotId = `${row}-slot-${targetPos}`; + const target = boardState[targetSlotId]; + + // Leeres Feld → weiter scannen + if (!target) continue; + + // Eigene Karte → Range geht hindurch (keine Aktion, weiter scannen) + if (target.owner === owner) continue; + + /* ── Feindliche Karte gefunden → Angriff ─────────────────── */ + const atk = card.attack ?? 0; + target.card = { + ...target.card, + defends: (target.card.defends ?? 0) - atk, + }; + + events.push({ + type : 'attack', + from : currentSlotId, + to : targetSlotId, + damage : atk, + remainingDef: target.card.defends, + }); + + // Karte sterben lassen wenn defends ≤ 0 + if (target.card.defends <= 0) { + delete boardState[targetSlotId]; + events.push({ + type : 'die', + slotId: targetSlotId, + }); + } + + break; // Nur die erste feindliche Karte pro Runde angreifen + } + } + + return events; +} + +module.exports = { runCombatPhase }; + + +/* ═══════════════════════════════════════════════════════════════════ + INTEGRATION IN DEN BESTEHENDEN SOCKET-SERVER + (nur als Referenz-Snippet – in die eigentliche arena-socket.js einbauen) +═══════════════════════════════════════════════════════════════════ */ + +/* + +const { runCombatPhase } = require('./combat'); + +// Pro Match einen boardState auf dem Server halten: +// matchBoards[matchId] = { [slotId]: { card, owner } } +const matchBoards = {}; +const matchLeftSlot = {}; // matchLeftSlot[matchId] = 'player1' | 'player2' + +// Wenn eine Karte gespielt wird → server-seitigen boardState aktualisieren: +socket.on('card_played', data => { + const { matchId, slot, boardSlot, card } = data; + if (!matchBoards[matchId]) matchBoards[matchId] = {}; + matchBoards[matchId][boardSlot] = { card, owner: slot }; + // ...weiter wie bisher (an Gegner broadcasten, boardSync etc.) +}); + +// leftSlot merken sobald er feststeht (z.B. in ready_status oder start_turn_request): +socket.on('start_turn_request', data => { + matchLeftSlot[data.matchId] = data.starterSlot; + // ...Zug starten wie bisher +}); + +// Nach end_turn: Kampfphase starten, dann Zug wechseln: +socket.on('end_turn', data => { + const { matchId, slot } = data; + const board = matchBoards[matchId] ?? {}; + const leftSlot = matchLeftSlot[matchId] ?? 'player1'; + + // Kampfphase berechnen + const events = runCombatPhase(board, leftSlot); + + // finalBoard als flaches Array für den boardSync senden + const finalBoard = Object.entries(board).map(([boardSlot, entry]) => ({ + boardSlot, + card : entry.card, + owner: entry.owner, + })); + + // An BEIDE Spieler senden (Reihenfolge & Ergebnis ist identisch) + io.to(matchId).emit('combat_phase', { events, finalBoard }); + + // Cooldowns / Zug wechseln wie bisher + // ... +}); + +*/