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
+ // ...
+});
+
+*/