dok/sockets/arena.socket.js
2026-04-14 09:30:27 +01:00

883 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
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 db = require('../database/database');
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 (const ev of avatarEvents) {
const target = ev.target; // 'player1' | 'player2'
// Sicherheits-Fallback: HP noch nicht initialisiert
if (room.hp[target] == null) {
console.warn(`[HP] room.hp[${target}] nicht gesetzt überspringe`);
continue;
}
// Schaden abziehen (niemals unter 0)
room.hp[target] = Math.max(0, room.hp[target] - (ev.damage ?? 0));
// Treffer an beide Clients melden
emitToMatch(io, matchId, 'avatar_damaged', {
slot : target,
damage : ev.damage,
remainingHp: room.hp[target],
maxHp : room.maxHp[target] ?? 20,
});
console.log(`[HP] ${target} -${ev.damage}${room.hp[target]}/${room.maxHp[target]}`);
// Match-Ende wenn HP auf 0
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 (!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 ── */
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 };