/* ============================================================ sockets/arena.socket.js 1v1 Matchmaking + 2v2 Team-Lobby inkl. Kampfphase nach end_turn ============================================================ */ const { runCombatPhase } = require('./combat'); const db = require('../database/database'); const { htAiMatchIds } = require('./1vKI_daily.socket'); const pointsRoute = require('../routes/points.route'); /* ── HP-Formel (muss mit arena.route.js übereinstimmen) ── */ function calcAvatarHp(level) { return 20 + (Math.max(1, Math.min(50, level || 1)) - 1) * 2; } const waitingPool = new Map(); const LEVEL_RANGE = 5; const READY_TIMEOUT = 30; /* ── 2v2 State ── */ const teams2v2 = new Map(); const readyTeams2v2 = new Map(); function generateId() { return `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; } /* ═══════════════════════════════════════════════════════════ HELPER: Generisch für 2v2 ═══════════════════════════════════════════════════════════ */ function getTeamMap(mode) { return teams2v2; } function getReadyMap(mode) { return readyTeams2v2; } function getMaxPlayers(mode) { return 2; } function broadcastLobbies(io, mode) { const teams = getTeamMap(mode); const max = getMaxPlayers(mode); const list = []; for (const [teamId, team] of teams) { if (team.players.length < max) { list.push({ teamId, leader: team.players[0]?.player?.name || "?", leaderLevel: team.players[0]?.player?.level || 1, count: team.players.length, max, }); } } io.emit(`${mode}_lobbies`, list); } function broadcastTeamStatus(io, teamId, mode) { const team = getTeamMap(mode).get(teamId); if (!team) return; const data = { teamId, leaderId: team.leaderId, players: team.players.map((p) => ({ socketId: p.socketId, name: p.player.name, level: p.player.level, ready: team.ready.has(p.socketId), })), count: team.players.length, max: getMaxPlayers(mode), }; for (const p of team.players) { io.to(p.socketId).emit(`${mode}_team_update`, data); } } function tryMatchmaking_nvn(io, mode) { const readyMap = getReadyMap(mode); const readyList = Array.from(readyMap.values()); if (readyList.length < 2) return; const team1 = readyList[0]; const team2 = readyList[1]; readyMap.delete(team1.id); readyMap.delete(team2.id); getTeamMap(mode).delete(team1.id); getTeamMap(mode).delete(team2.id); const matchId = `${mode}_${generateId()}`; const allPlayers = [ ...team1.players.map((p) => ({ team: 1, ...p })), ...team2.players.map((p) => ({ team: 2, ...p })), ]; for (const p of allPlayers) { const opponents = allPlayers .filter((x) => x.team !== p.team) .map((x) => x.player.name); const teammates = allPlayers .filter((x) => x.team === p.team && x.socketId !== p.socketId) .map((x) => x.player.name); const teamRef = p.team === 1 ? team1 : team2; const slotIndex = teamRef.players.findIndex((x) => x.socketId === p.socketId) + 1; io.to(p.socketId).emit(`match_found_${mode}`, { matchId, myTeam: p.team, teammates, opponents, mySlot: `team${p.team}_player${slotIndex}`, }); } console.log( `[${mode}] Match: Team1(${team1.players.map((p) => p.player.name)})` + ` vs Team2(${team2.players.map((p) => p.player.name)}) | ${matchId}`, ); } function leaveAllTeams_nvn(socketId, io, mode) { const teams = getTeamMap(mode); const ready = getReadyMap(mode); for (const [teamId, team] of teams) { const idx = team.players.findIndex((p) => p.socketId === socketId); if (idx === -1) continue; const playerName = team.players[idx].player.name; team.players.splice(idx, 1); team.ready.delete(socketId); ready.delete(teamId); if (team.players.length === 0) { teams.delete(teamId); console.log(`[${mode}] Team ${teamId} aufgelöst.`); } else { if (team.leaderId === socketId) { team.leaderId = team.players[0].socketId; console.log( `[${mode}] Neuer Leader in Team ${teamId}: ${team.players[0].player.name}`, ); } broadcastTeamStatus(io, teamId, mode); for (const p of team.players) { io.to(p.socketId).emit(`${mode}_partner_left`, { name: playerName }); } console.log(`[${mode}] ${playerName} hat Team ${teamId} verlassen.`); } broadcastLobbies(io, mode); break; } } /* ── Energie prüfen und abziehen ───────────────────────────── Gibt true zurück wenn genug Energie vorhanden, sonst false. Zieht ENERGY_COST ab und setzt energy_reset auf heute. ────────────────────────────────────────────────────────────── */ const ENERGY_COST = 2; const ENERGY_MAX = 40; async function deductEnergy(accountId) { if (!accountId) return true; // kein Account → erlauben (Fallback) try { const today = new Date().toISOString().slice(0, 10); // Energie laden + ggf. tagesreset const [[row]] = await db.query( "SELECT energy, energy_reset FROM account_currency WHERE account_id = ?", [accountId] ); if (!row) return true; // kein Eintrag → erlauben const lastReset = row.energy_reset ? new Date(row.energy_reset).toISOString().slice(0, 10) : null; const current = lastReset !== today ? ENERGY_MAX : (row.energy ?? ENERGY_MAX); if (current < ENERGY_COST) return false; // nicht genug Energie // Energie abziehen await db.query( `UPDATE account_currency SET energy = ?, energy_reset = ? WHERE account_id = ?`, [current - ENERGY_COST, today, accountId] ); return true; } catch (err) { console.error('[Energy] deductEnergy Fehler:', err); return true; // bei DB-Fehler nicht blockieren } } /* ── 1v1 Matchmaking ── */ async function tryMatchmaking(io, newSocketId) { const challenger = waitingPool.get(newSocketId); if (!challenger) return; for (const [id, entry] of waitingPool) { if (id === newSocketId) continue; const diff = Math.abs(entry.player.level - challenger.player.level); // Match nur wenn BEIDE Spieler die Leveldifferenz akzeptieren if (diff <= challenger.levelRange && diff <= entry.levelRange) { waitingPool.delete(newSocketId); waitingPool.delete(id); const matchId = `match_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; // Energie beider Spieler abziehen (2 pro Match) const [energyOk1, energyOk2] = await Promise.all([ deductEnergy(challenger.player.accountId), deductEnergy(entry.player.accountId), ]); if (!energyOk1) { challenger.socket.emit("queue_status", { status: "error", message: "Nicht genug Energie für einen Kampf. Warte bis morgen!", }); waitingPool.set(newSocketId, challenger); // zurück in Pool waitingPool.set(id, entry); return; } if (!energyOk2) { // Energie von Spieler 1 zurückgeben if (challenger.player.accountId) { db.query( "UPDATE account_currency SET energy = LEAST(energy + ?, ?) WHERE account_id = ?", [ENERGY_COST, ENERGY_MAX, challenger.player.accountId] ).catch(() => {}); } entry.socket.emit("queue_status", { status: "error", message: "Nicht genug Energie für einen Kampf. Warte bis morgen!", }); waitingPool.set(newSocketId, challenger); return; } challenger.socket.emit("match_found", { matchId, opponent: entry.player, mySlot: "player1", }); entry.socket.emit("match_found", { matchId, opponent: challenger.player, mySlot: "player2", }); console.log( `[1v1] ${challenger.player.name} vs ${entry.player.name} | ${matchId}`, ); return; } } } /* ── Bereit-Timer (1v1) ── */ function startReadyTimer(io, matchId) { if (!io._arenaTimers) io._arenaTimers = new Map(); if (io._arenaTimers.has(matchId)) return; let remaining = READY_TIMEOUT; emitToMatch(io, matchId, "ready_timer", { remaining }); const interval = setInterval(() => { remaining--; emitToMatch(io, matchId, "ready_timer", { remaining }); if (remaining <= 0) { clearInterval(interval); io._arenaTimers.delete(matchId); console.log(`[1v1] Match ${matchId} abgebrochen – Zeit abgelaufen.`); emitToMatch(io, matchId, "match_cancelled", { reason: "timeout", message: "Zeit abgelaufen.", }); } }, 1000); io._arenaTimers.set(matchId, interval); } function stopReadyTimer(io, matchId) { if (!io._arenaTimers) return; const interval = io._arenaTimers.get(matchId); if (interval) { clearInterval(interval); io._arenaTimers.delete(matchId); } } /* ═══════════════════════════════════════════════════════════ KAMPFPHASE HELPER Berechnet wie lange die Client-Animation dauert, damit turn_change erst NACH den Animationen gesendet wird. ═══════════════════════════════════════════════════════════ */ function calcCombatDuration(events) { const DELAY_BANNER = 600; const DELAY_MOVE = 350; const DELAY_ATTACK = 450; const DELAY_DIE = 300; const DELAY_AVATAR_ATTACK= 500; const DELAY_FINAL = 500; const BUFFER = 400; 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; if (ev.type === 'avatar_attack')total += DELAY_AVATAR_ATTACK; } 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 ═══════════════════════════════════════════════════════════ */ function registerTeamModeHandlers(io, socket, mode) { const max = getMaxPlayers(mode); /* Lobby-Liste anfordern */ socket.on(`get_${mode}_lobbies`, () => { const teams = getTeamMap(mode); const list = []; for (const [teamId, team] of teams) { if (team.players.length < max) { list.push({ teamId, leader: team.players[0]?.player?.name || "?", leaderLevel: team.players[0]?.player?.level || 1, count: team.players.length, max, }); } } socket.emit(`${mode}_lobbies`, list); }); /* Team erstellen */ socket.on(`create_${mode}_team`, (playerData) => { leaveAllTeams_nvn(socket.id, io, mode); const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1, }; const teamId = `team_${mode}_${generateId()}`; getTeamMap(mode).set(teamId, { id: teamId, leaderId: socket.id, players: [{ socketId: socket.id, player }], ready: new Set(), }); socket.emit(`${mode}_team_joined`, { teamId, isLeader: true }); broadcastTeamStatus(io, teamId, mode); broadcastLobbies(io, mode); console.log(`[${mode}] Team ${teamId} erstellt von ${player.name}`); }); /* Team beitreten */ socket.on(`join_${mode}_team`, (data) => { const { teamId, playerData } = data; const team = getTeamMap(mode).get(teamId); if (!team) return socket.emit(`${mode}_error`, { message: "Team nicht mehr verfügbar.", }); if (team.players.length >= max) return socket.emit(`${mode}_error`, { message: "Team ist bereits voll.", }); if (team.players.find((p) => p.socketId === socket.id)) return; leaveAllTeams_nvn(socket.id, io, mode); const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1, }; team.players.push({ socketId: socket.id, player }); socket.emit(`${mode}_team_joined`, { teamId, isLeader: false }); broadcastTeamStatus(io, teamId, mode); broadcastLobbies(io, mode); console.log(`[${mode}] ${player.name} hat Team ${teamId} beigetreten`); }); /* Team verlassen */ socket.on(`leave_${mode}_team`, () => { leaveAllTeams_nvn(socket.id, io, mode); }); /* ── Kick (nur Leader) ── */ socket.on(`kick_from_${mode}_team`, (data) => { const { teamId, targetSocketId } = data; const team = getTeamMap(mode).get(teamId); if (!team) return; if (team.leaderId !== socket.id) { return socket.emit(`${mode}_error`, { message: "Nur der Team-Leader kann Spieler entfernen.", }); } if (targetSocketId === socket.id) return; const idx = team.players.findIndex((p) => p.socketId === targetSocketId); if (idx === -1) return; const kickedName = team.players[idx].player.name; team.players.splice(idx, 1); team.ready.delete(targetSocketId); getReadyMap(mode).delete(teamId); io.to(targetSocketId).emit(`${mode}_kicked`, { message: "Du wurdest vom Team-Leader aus dem Team entfernt.", }); broadcastTeamStatus(io, teamId, mode); broadcastLobbies(io, mode); console.log(`[${mode}] ${kickedName} wurde aus Team ${teamId} gekickt.`); }); /* Bereit klicken */ socket.on(`${mode}_player_ready`, (data) => { const { teamId } = data; const team = getTeamMap(mode).get(teamId); if (!team || team.players.length < max) return; team.ready.add(socket.id); broadcastTeamStatus(io, teamId, mode); console.log( `[${mode}] Bereit in Team ${teamId}: ${team.ready.size}/${max}`, ); if (team.ready.size >= max) { getReadyMap(mode).set(teamId, team); for (const p of team.players) { io.to(p.socketId).emit(`${mode}_searching`, { message: "Suche nach Gegnerteam…", }); } console.log(`[${mode}] Team ${teamId} sucht Gegner.`); tryMatchmaking_nvn(io, mode); } }); } /* ── Hilfsfunktionen: Direkte Socket-Zustellung ── */ function getRoom(io, matchId) { return io._arenaRooms?.get(matchId) || null; } function emitToMatch(io, matchId, event, data) { const room = getRoom(io, matchId); if (!room) { console.warn(`[emitToMatch] Kein Room für ${matchId}`); return; } ["player1", "player2"].forEach((slot) => { if (room.sockets[slot]) { io.to(room.sockets[slot]).emit(event, data); console.log(`[emitToMatch] ${event} → ${slot} (${room.sockets[slot]})`); } }); } function emitToOpponent(io, matchId, senderSlot, event, data) { const room = getRoom(io, matchId); if (!room) return; const oppSlot = senderSlot === "player1" ? "player2" : "player1"; if (room.sockets[oppSlot]) { io.to(room.sockets[oppSlot]).emit(event, data); } } /* ═══════════════════════════════════════════════════════════ AVATAR-HP HELPERS ═══════════════════════════════════════════════════════════ */ /** HP für beide Spieler beim Spielstart initialisieren. * Lookup aus arena_avatar_levels, Fallback auf Formel. * HP-Tracking läuft rein im Speicher (room.hp / room.maxHp). */ async function initMatchHP(io, matchId, room) { for (const slot of ['player1', 'player2']) { const accountId = room.accountIds[slot]; let maxHp = 20; // Fallback try { const [[acc]] = await db.query( "SELECT level FROM accounts WHERE id = ?", [accountId || 0] ); const level = acc?.level ?? 1; // Aus Lookup-Tabelle lesen, Formel als Fallback const [[row]] = await db.query( "SELECT max_hp FROM arena_avatar_levels WHERE level = ?", [level] ); maxHp = row?.max_hp ?? calcAvatarHp(level); } catch (err) { console.error(`[HP] Level-Lookup Fehler (${slot}):`, err); // Formel-Fallback – Spiel läuft trotzdem weiter } room.hp[slot] = maxHp; room.maxHp[slot] = maxHp; } // Initiale HP an beide Clients senden emitToMatch(io, matchId, 'hp_init', { hp: room.hp, maxHp: room.maxHp }); console.log(`[HP] Init ${matchId}: P1=${room.hp.player1}/${room.maxHp.player1}, P2=${room.hp.player2}/${room.maxHp.player2}`); } /** Avatar-Treffer aus Combat-Events verarbeiten, Match bei Tod beenden. * HP wird nur im Speicher (room.hp) verwaltet – kein DB-Schreiben. */ async function processAvatarAttacks(io, matchId, room, events) { const avatarEvents = events.filter(e => e.type === 'avatar_attack'); if (avatarEvents.length === 0) return false; for (let i = 0; i < avatarEvents.length; i++) { // Mehrere Avatar-Treffer im gleichen Zug: 500ms Abstand if (i > 0) await new Promise(r => setTimeout(r, 500)); const ev = avatarEvents[i]; const target = ev.target; if (room.hp[target] == null) { console.warn(`[HP] room.hp[${target}] nicht gesetzt – überspringe`); continue; } room.hp[target] = Math.max(0, room.hp[target] - (ev.damage ?? 0)); emitToMatch(io, matchId, 'avatar_damaged', { slot : target, damage : ev.damage, remainingHp: room.hp[target], maxHp : room.maxHp[target] ?? 20, row : ev.from?.split('-slot-')[0] ?? 'row1', // Reihe für Versatz }); console.log(`[HP] ${target} (${ev.from}) -${ev.damage} → ${room.hp[target]}/${room.maxHp[target]}`); if (room.hp[target] <= 0) { await handleMatchEnd(io, matchId, room, target); return true; } } return false; } /** Match beenden: Ergebnis emitieren + 15 Punkte an Gewinner vergeben */ async function handleMatchEnd(io, matchId, room, loserSlot) { if (room.gameOver) return; room.gameOver = true; const winnerSlot = loserSlot === 'player1' ? 'player2' : 'player1'; const winnerAccId = room.accountIds[winnerSlot]; const loserAccId = room.accountIds[loserSlot]; console.log(`[Match] Ende: Gewinner=${winnerSlot} (${winnerAccId}), Verlierer=${loserSlot} (${loserAccId}) | ${matchId}`); // Gewinner: 15 Punkte | Verlierer: 5 Punkte let winnerResult = { awarded: 0 }; let loserResult = { awarded: 0 }; if (winnerAccId) { try { winnerResult = await pointsRoute.awardPoints(winnerAccId, 15); } catch (err) { console.error('[Match] Gewinner-Punkte Fehler:', err); } } if (loserAccId) { try { loserResult = await pointsRoute.awardPoints(loserAccId, 5); } catch (err) { console.error('[Match] Verlierer-Punkte Fehler:', err); } } // Ergebnis an beide Clients senden const room_ = getRoom(io, matchId); if (room_.sockets[winnerSlot]) { io.to(room_.sockets[winnerSlot]).emit('match_result', { won : true, awarded : winnerResult.awarded ?? 0, level_up : winnerResult.level_up ?? false, new_level: winnerResult.new_level ?? null, }); } if (room_.sockets[loserSlot]) { io.to(room_.sockets[loserSlot]).emit('match_result', { won : false, awarded : loserResult.awarded ?? 0, level_up : loserResult.level_up ?? false, new_level: loserResult.new_level ?? null, }); } // HP läuft nur im Speicher – kein DB-Cleanup nötig } /* ═══════════════════════════════════════════════════════════ HAUPT-HANDLER ═══════════════════════════════════════════════════════════ */ function registerArenaHandlers(io, socket) { /* ── 1v1 ── */ socket.on("join_1v1", (playerData) => { if (waitingPool.has(socket.id)) return; const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1, accountId: playerData.id || null, // für Energie-Abzug }; // levelRange vom Client übernehmen (5 oder 10), Standard = 5 const levelRange = [5, 10].includes(Number(playerData.levelRange)) ? Number(playerData.levelRange) : LEVEL_RANGE; waitingPool.set(socket.id, { socket, player, levelRange }); socket.emit("queue_status", { status: "waiting", poolSize: waitingPool.size, }); console.log( `[1v1] ${player.name} (Lvl ${player.level}) im Pool. Größe: ${waitingPool.size}`, ); tryMatchmaking(io, socket.id); }); socket.on("leave_1v1", () => { if (waitingPool.delete(socket.id)) socket.emit("queue_status", { status: "left" }); }); socket.on("arena_join", (data) => { const { matchId, slot, playerName } = data; console.log( `[1v1] arena_join empfangen: matchId=${matchId}, slot=${slot}, name=${playerName}, socketId=${socket.id}`, ); if (!matchId || !slot) { console.warn(`[1v1] arena_join abgewiesen – matchId oder slot fehlt`); return; } if (htAiMatchIds.has(matchId)) return; // wird von himmelstor.socket.js behandelt if (!io._arenaRooms) io._arenaRooms = new Map(); if (!io._arenaRooms.has(matchId)) { io._arenaRooms.set(matchId, { sockets : {}, names : {}, accountIds : {}, // { player1: accountId, player2: accountId } boardCards : [], boardState : {}, leftSlot : null, hp : {}, // { player1: current, player2: current } in-memory maxHp : {}, // { player1: max, player2: max } gameOver : false, }); } const room = io._arenaRooms.get(matchId); room.sockets[slot] = socket.id; if (playerName && playerName !== "Spieler") { room.names[slot] = playerName; } else if (!room.names[slot] || room.names[slot] === "Spieler") { room.names[slot] = playerName || "Spieler"; } // Account-ID speichern (wird für HP-Init und Punkte-Vergabe benötigt) if (data.accountId) room.accountIds[slot] = data.accountId; console.log( `[1v1] Name gesetzt: slot=${slot}, name=${room.names[slot]}, playerName=${playerName}`, ); socket.join("arena_" + matchId); if (room.boardCards?.length > 0) { socket.emit("board_sync", { cards: room.boardCards }); } const otherSlot = slot === "player1" ? "player2" : "player1"; if (room.sockets[otherSlot]) { console.log( `[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", boardSync: room.boardCards || [], hp : room.hp || {}, maxHp : room.maxHp || {}, }); startReadyTimer(io, matchId); } else { console.log( `[1v1] Erster Spieler joined, warte auf zweiten | Match ${matchId}`, ); socket .to("arena_" + matchId) .emit("arena_opponent_joined", { name: room.names[slot], slot }); } }); socket.on("player_ready", (data) => { const { matchId, slot } = data; if (!matchId || !slot) return; if (!io._arenaReady) io._arenaReady = new Map(); if (!io._arenaReady.has(matchId)) io._arenaReady.set(matchId, new Set()); const readySet = io._arenaReady.get(matchId); readySet.add(slot); const readyData = { readyCount: readySet.size, readySlots: Array.from(readySet), }; emitToMatch(io, matchId, "ready_status", readyData); console.log( `[1v1] ready_status: ${readySet.size}/2 bereit | Match ${matchId}`, ); if (readySet.size >= 2) { stopReadyTimer(io, matchId); io._arenaReady.delete(matchId); console.log( `[1v1] Beide bereit – warte auf start_turn_request | Match ${matchId}`, ); } }); socket.on("player_surrender", (data) => { const { matchId, slot } = data; io.to("arena_" + matchId).emit("player_surrendered", { slot }); }); /* ── Karte gespielt ── */ socket.on("card_played", (data) => { const { matchId, slot, boardSlot, card } = data; if (!matchId) return; // An Gegner senden emitToOpponent(io, matchId, slot, "card_played", 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: ${card?.name} → ${boardSlot} (owner: ${slot}) | Match ${matchId}`, ); }); /* ── Startspieler festlegen ── */ socket.on("start_turn_request", async (data) => { const { matchId, starterSlot } = data; if (!matchId || !starterSlot) return; if (htAiMatchIds.has(matchId)) return; // KI-Match: turn wird von himmelstor.socket gestartet if (!io._turnInit) io._turnInit = new Set(); 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; // Avatar-HP initialisieren await initMatchHP(io, matchId, room); const boardCards = room?.boardCards || []; emitToMatch(io, matchId, "turn_change", { activeSlot: starterSlot, boardSync : boardCards, hp : room.hp, maxHp : room.maxHp, }); console.log( `[1v1] Spiel startet → ${starterSlot} (linker Spieler) beginnt | Match ${matchId}`, ); }); /* ── Zug beenden → Kampfphase → Zugwechsel ── */ /* ── Karte vom Board abwerfen ────────────────────────────── */ socket.on('discard_card', (data) => { const { matchId, slotId, slot } = data; if (!matchId || !slotId) return; if (htAiMatchIds.has(matchId)) return; // KI-Matches: handled by himmelstor.socket const room = io._arenaRooms?.get(matchId); if (!room || room.gameOver) return; if (room.sockets[slot] !== socket.id) return; // nur eigene Karten // Karte aus boardState entfernen if (room.boardState) delete room.boardState[slotId]; room.boardCards = room.boardCards?.filter(c => c.boardSlot !== slotId) ?? []; // Gegner informieren const oppSlot = slot === 'player1' ? 'player2' : 'player1'; const oppSocket = room.sockets[oppSlot]; if (oppSocket) { io.to(oppSocket).emit('card_discarded', { slotId }); } console.log(`[Arena] Karte abgeworfen: ${slotId} von ${slot} | Match ${matchId}`); }); socket.on("end_turn", async (data) => { const { matchId, slot } = data; if (!matchId || !slot) return; if (htAiMatchIds.has(matchId)) return; // wird von himmelstor.socket.js behandelt 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, slot); // slot = aktiver Spieler // 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}`, ); // Avatar-Schaden aus Combat-Events verarbeiten const matchEnded = await processAvatarAttacks(io, matchId, room, combatEvents); if (matchEnded) return; // Match vorbei – kein turn_change mehr // Zugwechsel NACH den Client-Animationen senden const animDuration = calcCombatDuration(combatEvents); setTimeout(() => { if (room.gameOver) return; emitToMatch(io, matchId, "turn_change", { activeSlot: nextSlot, boardSync : room.boardCards, hp : room.hp, maxHp : room.maxHp, }); console.log( `[1v1] turn_change gesendet nach ${animDuration}ms | ${slot} → ${nextSlot} | Match ${matchId}`, ); }, animDuration); }); /* ── 2v2 ── */ registerTeamModeHandlers(io, socket, "2v2"); /* ── Disconnect ── */ socket.on("disconnect", () => { if (waitingPool.delete(socket.id)) { console.log(`[1v1] ${socket.id} disconnected – aus Pool entfernt.`); } leaveAllTeams_nvn(socket.id, io, "2v2"); }); } module.exports = { registerArenaHandlers };