gc,lhgtck
This commit is contained in:
parent
3f5269862b
commit
a85ac02f25
@ -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
|
||||||
═══════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
|||||||
@ -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
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