gc,lhgtck

This commit is contained in:
cay 2026-04-13 17:21:54 +01:00
parent 3f5269862b
commit a85ac02f25
3 changed files with 481 additions and 59 deletions

View File

@ -1,4 +1,4 @@
/* /*
public/js/buildings/1v1.js public/js/buildings/1v1.js
Vollstaendige Spielfeld-Logik fuer das 1v1-Battlefield. Vollstaendige Spielfeld-Logik fuer das 1v1-Battlefield.
EJS-Variablen kommen aus window.GAME_CONFIG (in der EJS gesetzt). 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 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 = {}; const boardState = {};
function buildStatsHtml(card) { function buildStatsHtml(card) {
@ -314,6 +320,15 @@ function renderCardInSlot(slot, card) {
: `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:4px;font-family:Cinzel,serif;padding:4px;"><span style="font-size:18px;">\u2694\uFE0F</span><span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span></div>${buildStatsHtml(card)}`; : `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:4px;font-family:Cinzel,serif;padding:4px;"><span style="font-size:18px;">\u2694\uFE0F</span><span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span></div>${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 = '<span class="slot-icon">\u2736</span><span class="slot-num">' + slotId.split('-slot-')[1] + '</span>';
}
/* /*
SOCKET & ARENA-JOIN SOCKET & ARENA-JOIN
@ -356,12 +371,17 @@ const readyFallbackTimer = setTimeout(() => {
}, 10000); }, 10000);
/* ── Board-Sync ──────────────────────────────────────────── */ /* ── 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) { function applyBoardSync(cards) {
if (!cards || !cards.length) return; if (!cards || !cards.length) return;
cards.forEach(cd => { cards.forEach(cd => {
const slotEl = document.getElementById(cd.boardSlot); const slotEl = document.getElementById(cd.boardSlot);
if (!slotEl || boardState[cd.boardSlot]) return; 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); renderCardOnBoard(slotEl, cd.card);
}); });
console.log('[1v1] Board sync:', cards.length, 'Karten'); console.log('[1v1] Board sync:', cards.length, 'Karten');
@ -565,7 +585,9 @@ document.getElementById('handArea').addEventListener('dragend', e => {
handSlotState[sourceId] = null; handSlotState[sourceId] = null;
renderHandSlot(sourceId); renderHandSlot(sourceId);
boardState[slot.id] = cardState.card;
/* ── boardState mit owner speichern ─────────────────── */
boardState[slot.id] = { card: cardState.card, owner: mySlot };
renderCardOnBoard(slot, cardState.card); renderCardOnBoard(slot, cardState.card);
slot.classList.remove('drop-zone-active', 'drop-zone-hover'); slot.classList.remove('drop-zone-active', 'drop-zone-hover');
@ -582,11 +604,171 @@ socket.on('card_played', data => {
if (boardState[data.boardSlot]) return; if (boardState[data.boardSlot]) return;
const slotEl = document.getElementById(data.boardSlot); const slotEl = document.getElementById(data.boardSlot);
if (!slotEl) { console.warn('[1v1] card_played: Slot fehlt:', data.boardSlot); return; } 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); renderCardOnBoard(slotEl, data.card);
console.log('[1v1] Gegner Karte:', data.card?.name, '->', data.boardSlot); 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 = '<span class="slot-icon">\u2736</span><span class="slot-num">' + ev.from.split('-slot-')[1] + '</span>';
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 AUFGEBEN
*/ */

View File

@ -1,9 +1,11 @@
/* ============================================================ /* ============================================================
sockets/arena.js sockets/arena.socket.js
1v1 Matchmaking + 2v2 Team-Lobby + 4v4 Team-Lobby 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 waitingPool = new Map();
const LEVEL_RANGE = 5; const LEVEL_RANGE = 5;
const READY_TIMEOUT = 30; const READY_TIMEOUT = 30;
@ -56,9 +58,9 @@ function broadcastTeamStatus(io, teamId, mode) {
if (!team) return; if (!team) return;
const data = { const data = {
teamId, teamId,
leaderId: team.leaderId, // ← Leader-SocketId mitschicken leaderId: team.leaderId,
players: team.players.map((p) => ({ players: team.players.map((p) => ({
socketId: p.socketId, // ← für Kick-Button im Frontend socketId: p.socketId,
name: p.player.name, name: p.player.name,
level: p.player.level, level: p.player.level,
ready: team.ready.has(p.socketId), ready: team.ready.has(p.socketId),
@ -133,7 +135,6 @@ function leaveAllTeams_nvn(socketId, io, mode) {
teams.delete(teamId); teams.delete(teamId);
console.log(`[${mode}] Team ${teamId} aufgelöst.`); console.log(`[${mode}] Team ${teamId} aufgelöst.`);
} else { } else {
// Falls Leader das Team verlässt → nächsten Spieler zum Leader machen
if (team.leaderId === socketId) { if (team.leaderId === socketId) {
team.leaderId = team.players[0].socketId; team.leaderId = team.players[0].socketId;
console.log( 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 Handler-Generator für 2v2 und 4v4
*/ */
@ -253,7 +287,7 @@ function registerTeamModeHandlers(io, socket, mode) {
getTeamMap(mode).set(teamId, { getTeamMap(mode).set(teamId, {
id: teamId, id: teamId,
leaderId: socket.id, // ← Ersteller wird Leader leaderId: socket.id,
players: [{ socketId: socket.id, player }], players: [{ socketId: socket.id, player }],
ready: new Set(), ready: new Set(),
}); });
@ -306,14 +340,12 @@ function registerTeamModeHandlers(io, socket, mode) {
if (!team) return; if (!team) return;
// Nur der Leader darf kicken
if (team.leaderId !== socket.id) { if (team.leaderId !== socket.id) {
return socket.emit(`${mode}_error`, { return socket.emit(`${mode}_error`, {
message: "Nur der Team-Leader kann Spieler entfernen.", message: "Nur der Team-Leader kann Spieler entfernen.",
}); });
} }
// Sich selbst kicken nicht erlaubt
if (targetSocketId === socket.id) return; if (targetSocketId === socket.id) return;
const idx = team.players.findIndex((p) => p.socketId === targetSocketId); const idx = team.players.findIndex((p) => p.socketId === targetSocketId);
@ -324,7 +356,6 @@ function registerTeamModeHandlers(io, socket, mode) {
team.ready.delete(targetSocketId); team.ready.delete(targetSocketId);
getReadyMap(mode).delete(teamId); getReadyMap(mode).delete(teamId);
// Gekickten Spieler benachrichtigen
io.to(targetSocketId).emit(`${mode}_kicked`, { io.to(targetSocketId).emit(`${mode}_kicked`, {
message: "Du wurdest vom Team-Leader aus dem Team entfernt.", message: "Du wurdest vom Team-Leader aus dem Team entfernt.",
}); });
@ -364,7 +395,6 @@ function getRoom(io, matchId) {
return io._arenaRooms?.get(matchId) || null; return io._arenaRooms?.get(matchId) || null;
} }
// Sendet an beide Spieler über ihre gespeicherten Socket-IDs
function emitToMatch(io, matchId, event, data) { function emitToMatch(io, matchId, event, data) {
const room = getRoom(io, matchId); const room = getRoom(io, matchId);
if (!room) { 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) { function emitToOpponent(io, matchId, senderSlot, event, data) {
const room = getRoom(io, matchId); const room = getRoom(io, matchId);
if (!room) return; if (!room) return;
@ -428,22 +457,24 @@ function registerArenaHandlers(io, socket) {
} }
if (!io._arenaRooms) io._arenaRooms = new Map(); if (!io._arenaRooms) io._arenaRooms = new Map();
if (!io._arenaRooms.has(matchId)) if (!io._arenaRooms.has(matchId)) {
io._arenaRooms.set(matchId, { sockets: {}, names: {}, boardCards: [] }); 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); const room = io._arenaRooms.get(matchId);
room.sockets[slot] = socket.id; 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") { if (playerName && playerName !== "Spieler") {
// Guter Name vom Client → immer übernehmen
room.names[slot] = playerName; room.names[slot] = playerName;
} else if (!room.names[slot] || room.names[slot] === "Spieler") { } else if (!room.names[slot] || room.names[slot] === "Spieler") {
// Kein guter Name bekannt → Fallback
room.names[slot] = playerName || "Spieler"; room.names[slot] = playerName || "Spieler";
} }
// Sonst: bereits guter Name gespeichert → nicht überschreiben
console.log( console.log(
`[1v1] Name gesetzt: slot=${slot}, name=${room.names[slot]}, playerName=${playerName}`, `[1v1] Name gesetzt: slot=${slot}, name=${room.names[slot]}, playerName=${playerName}`,
@ -451,7 +482,6 @@ function registerArenaHandlers(io, socket) {
socket.join("arena_" + matchId); socket.join("arena_" + matchId);
// Bereits gespielte Karten an neu verbundenen Spieler senden
if (room.boardCards?.length > 0) { if (room.boardCards?.length > 0) {
socket.emit("board_sync", { cards: room.boardCards }); socket.emit("board_sync", { cards: room.boardCards });
} }
@ -462,8 +492,8 @@ function registerArenaHandlers(io, socket) {
`[1v1] Beide Spieler da → arena_ready senden | Match ${matchId}`, `[1v1] Beide Spieler da → arena_ready senden | Match ${matchId}`,
); );
emitToMatch(io, matchId, "arena_ready", { emitToMatch(io, matchId, "arena_ready", {
player1: room.names["player1"] || "Spieler 1", player1 : room.names["player1"] || "Spieler 1",
player2: room.names["player2"] || "Spieler 2", player2 : room.names["player2"] || "Spieler 2",
boardSync: room.boardCards || [], boardSync: room.boardCards || [],
}); });
startReadyTimer(io, matchId); startReadyTimer(io, matchId);
@ -496,8 +526,6 @@ function registerArenaHandlers(io, socket) {
if (readySet.size >= 2) { if (readySet.size >= 2) {
stopReadyTimer(io, matchId); stopReadyTimer(io, matchId);
io._arenaReady.delete(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( console.log(
`[1v1] Beide bereit warte auf start_turn_request | Match ${matchId}`, `[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 }); io.to("arena_" + matchId).emit("player_surrendered", { slot });
}); });
/* ── Zugwechsel ── */ /* ── Karte gespielt ── */
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 ── */
socket.on("card_played", (data) => { socket.on("card_played", (data) => {
const { matchId, slot } = data; const { matchId, slot, boardSlot, card } = data;
if (!matchId) return; if (!matchId) return;
// Direkt an Gegner-Socket senden // An Gegner senden
emitToOpponent(io, matchId, slot, "card_played", data); emitToOpponent(io, matchId, slot, "card_played", data);
// Karte im Server-State speichern (für Reconnect/board_sync) // Im Server-State speichern
const roomState = io._arenaRooms?.get(matchId); const room = io._arenaRooms?.get(matchId);
if (roomState) { if (room) {
roomState.boardCards = roomState.boardCards || []; // boardState mit Owner speichern (für Kampfphase)
roomState.boardCards.push(data); room.boardState[boardSlot] = { card, owner: slot };
// boardCards neu aufbauen (für Reconnect/boardSync)
room.boardCards = boardStateToCards(room.boardState);
} }
console.log( 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) => { socket.on("start_turn_request", (data) => {
const { matchId, starterSlot } = data; const { matchId, starterSlot } = data;
if (!matchId || !starterSlot) return; if (!matchId || !starterSlot) return;
if (!io._turnInit) io._turnInit = new Set(); if (!io._turnInit) io._turnInit = new Set();
// Nur einmal pro Match ausführen
if (io._turnInit.has(matchId)) return; if (io._turnInit.has(matchId)) return;
io._turnInit.add(matchId); io._turnInit.add(matchId);
setTimeout(() => io._turnInit?.delete(matchId), 60000); setTimeout(() => io._turnInit?.delete(matchId), 60000);
// NEU: linken Spieler im Room merken (wird für Kampfphase benötigt)
const room = io._arenaRooms?.get(matchId); const room = io._arenaRooms?.get(matchId);
if (room) room.leftSlot = starterSlot;
const boardCards = room?.boardCards || []; const boardCards = room?.boardCards || [];
emitToMatch(io, matchId, "turn_change", { emitToMatch(io, matchId, "turn_change", {
activeSlot: starterSlot, activeSlot: starterSlot,
boardSync: boardCards, boardSync : boardCards,
}); });
console.log( console.log(
`[1v1] Spiel startet → ${starterSlot} (linker Spieler) beginnt | Match ${matchId}`, `[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 ── */ /* ── 2v2 & 4v4 ── */
registerTeamModeHandlers(io, socket, "2v2"); registerTeamModeHandlers(io, socket, "2v2");
registerTeamModeHandlers(io, socket, "4v4"); registerTeamModeHandlers(io, socket, "4v4");

182
sockets/combat.js Normal file
View File

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