gc,lhgtck
This commit is contained in:
parent
3f5269862b
commit
a85ac02f25
@ -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) {
|
||||
: `<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
|
||||
@ -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 = '<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
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
@ -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");
|
||||
|
||||
182
sockets/combat.js
Normal file
182
sockets/combat.js
Normal 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
|
||||
// ...
|
||||
});
|
||||
|
||||
*/
|
||||
Loading…
Reference in New Issue
Block a user