This commit is contained in:
cay 2026-04-07 15:32:44 +01:00
parent c92266e23e
commit c61cddddb6
3 changed files with 1108 additions and 117 deletions

View File

@ -4,33 +4,56 @@ export async function loadArena() {
ui.innerHTML = `
<div id="arena-ui">
<div class="arena-title"> Kampfarena</div>
<p class="arena-subtitle">Wähle deinen Kampfmodus</p>
<div class="arena-modes">
<div class="arena-mode-card" data-mode="1v1">
<div class="arena-mode-icon">🗡</div>
<div class="arena-mode-label">1v1</div>
<div class="arena-mode-desc">Einzelkampf Beweis deine Stärke im Duell</div>
<!-- Modus-Auswahl -->
<div id="arena-mode-screen">
<div class="arena-title"> Kampfarena</div>
<p class="arena-subtitle">Wähle deinen Kampfmodus</p>
<div class="arena-modes">
<div class="arena-mode-card" data-mode="1v1">
<div class="arena-mode-icon">🗡</div>
<div class="arena-mode-label">1v1</div>
<div class="arena-mode-desc">Einzelkampf Beweis deine Stärke im Duell</div>
</div>
<div class="arena-mode-card" data-mode="2v2">
<div class="arena-mode-icon"></div>
<div class="arena-mode-label">2v2</div>
<div class="arena-mode-desc">Verbünde dich mit einem Kameraden im Kampf</div>
</div>
<div class="arena-mode-card" data-mode="4v4">
<div class="arena-mode-icon">🛡</div>
<div class="arena-mode-label">4v4</div>
<div class="arena-mode-desc">Schlachtruf Führe deine Truppe zum Sieg</div>
</div>
</div>
<div class="arena-mode-card" data-mode="2v2">
<div class="arena-mode-icon"></div>
<div class="arena-mode-label">2v2</div>
<div class="arena-mode-desc">Verbünde dich mit einem Kameraden im Kampf</div>
</div>
<div class="arena-mode-card" data-mode="4v4">
<div class="arena-mode-icon">🛡</div>
<div class="arena-mode-label">4v4</div>
<div class="arena-mode-desc">Schlachtruf Führe deine Truppe zum Sieg</div>
</div>
<div id="arena-queue-status" style="display:none;"></div>
</div>
<!-- Status-Anzeige (erscheint nur während Suche) -->
<div id="arena-queue-status" style="display:none;"></div>
<!-- 2v2 Lobby-Screen -->
<div id="arena-2v2-screen" style="display:none;">
<div class="arena-lobby-header">
<button class="arena-back-btn" id="arena-2v2-back"> Zurück</button>
<div class="arena-title" style="margin:0;"> 2v2 Team-Lobby</div>
</div>
<!-- Team-Panel (erscheint nach Beitritt) -->
<div id="arena-team-panel" style="display:none;">
<div class="arena-team-box">
<div class="arena-team-title">Dein Team</div>
<div id="arena-team-players"></div>
<div id="arena-team-actions"></div>
</div>
<div id="arena-team-status"></div>
</div>
<!-- Lobby-Liste -->
<div id="arena-lobby-section">
<div class="arena-lobby-actions">
<button class="arena-btn-create" id="arena-create-team-btn"> Eigenes Team erstellen</button>
</div>
<div class="arena-lobby-title">Offene Teams</div>
<div id="arena-lobby-list"><div class="arena-lobby-empty">Keine offenen Teams vorhanden.</div></div>
</div>
</div>
</div>
`;
@ -214,6 +237,146 @@ function injectArenaStyles() {
pointer-events: none;
border-color: rgba(255,215,80,0.5) !important;
}
/* ── 2v2 Lobby Screen ── */
#arena-2v2-screen {
flex-direction: column;
gap: 14px;
padding: 16px;
height: 100%;
overflow-y: auto;
}
.arena-lobby-header {
display: flex;
align-items: center;
gap: 16px;
}
.arena-back-btn {
background: none;
border: 1px solid rgba(255,200,80,0.3);
color: #c8960c;
font-family: "Cinzel", serif;
font-size: 12px;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
}
.arena-back-btn:hover { background: rgba(200,150,12,0.15); }
.arena-lobby-actions { display: flex; gap: 10px; margin-bottom: 4px; }
.arena-btn-create {
background: linear-gradient(#4a3018, #2a1a08);
border: 2px solid #8b6a3c;
border-radius: 7px;
color: #f0d9a6;
font-family: "Cinzel", serif;
font-size: 12px;
padding: 8px 16px;
cursor: pointer;
transition: 0.2s;
}
.arena-btn-create:hover { border-color: #f0d060; }
.arena-lobby-title {
font-family: "Cinzel", serif;
font-size: 13px;
color: #a08060;
letter-spacing: 1px;
margin-bottom: 6px;
}
.arena-lobby-empty {
font-family: "Cinzel", serif;
font-size: 12px;
color: #606060;
padding: 12px 0;
}
.arena-lobby-row {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(#2a1a08, #1a0f04);
border: 1px solid #6b4b2a;
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 6px;
}
.arena-lobby-row-info { display: flex; align-items: center; gap: 12px; }
.arena-lobby-leader { font-family: "Cinzel", serif; font-size: 13px; color: #f0d9a6; }
.arena-lobby-level { font-size: 11px; color: #a08060; }
.arena-lobby-count { font-size: 11px; color: #6a9a4a; }
.arena-btn-join {
background: linear-gradient(#1a4a18, #0f2a0e);
border: 2px solid #4a8a3c;
border-radius: 6px;
color: #a0e090;
font-family: "Cinzel", serif;
font-size: 11px;
padding: 5px 12px;
cursor: pointer;
transition: 0.2s;
}
.arena-btn-join:hover { border-color: #8ae060; color: #c0f0a0; }
/* ── Team-Panel ── */
.arena-team-box {
background: linear-gradient(#2a1a08, #1a0f04);
border: 2px solid #6b4b2a;
border-radius: 10px;
padding: 14px;
}
.arena-team-title {
font-family: "Cinzel", serif;
font-size: 13px;
color: #f0d060;
letter-spacing: 2px;
margin-bottom: 10px;
}
.arena-team-player {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
border: 1px solid #3a2810;
border-radius: 6px;
margin-bottom: 5px;
background: rgba(255,255,255,0.03);
}
.arena-team-player.ready { border-color: #4a8a3c; background: rgba(74,138,60,0.1); }
.arena-team-player-name { font-family: "Cinzel", serif; font-size: 12px; color: #f0d9a6; flex: 1; }
.arena-team-player-level { font-size: 11px; color: #a08060; }
.arena-team-player-status { font-size: 11px; }
.arena-waiting-partner {
font-family: "Cinzel", serif;
font-size: 11px;
color: #a08060;
text-align: center;
padding: 8px;
animation: pulse 2s ease-in-out infinite;
}
.arena-btn-ready {
width: 100%;
margin-top: 10px;
background: linear-gradient(#1a4a18, #0f2a0e);
border: 2px solid #4a8a3c;
border-radius: 8px;
color: #a0e090;
font-family: "Cinzel", serif;
font-size: 14px;
padding: 10px;
cursor: pointer;
transition: 0.2s;
}
.arena-btn-ready:hover:not([disabled]) { border-color: #8ae060; background: linear-gradient(#2a6a28, #1a3a18); }
.arena-btn-ready.active { border-color: #8ae060; color: #c0f0a0; cursor: default; }
.arena-searching-box {
font-family: "Cinzel", serif;
font-size: 12px;
color: #dceb15;
text-align: center;
padding: 10px;
border: 1px solid rgba(255,215,80,0.3);
border-radius: 8px;
margin-top: 8px;
animation: pulse 2s ease-in-out infinite;
}
`;
document.head.appendChild(style);
}
@ -229,17 +392,186 @@ function initArenaModes() {
document.querySelectorAll(".arena-mode-card").forEach((card) => {
card.addEventListener("click", () => {
const mode = card.dataset.mode;
if (mode === "1v1") {
handle1v1Click(card);
} else if (mode === "2v2") {
open2v2Lobby();
} else {
console.log("Arena Modus gewählt:", mode);
// Platzhalter für 2v2 / 4v4
}
});
});
}
/*
2v2 LOBBY
*/
let my2v2TeamId = null;
let myArenaData = null;
async function open2v2Lobby() {
const socket = getSocket();
if (!socket) { showArenaError("Keine Verbindung zum Server."); return; }
// Spielerdaten laden
if (!myArenaData) {
try {
const res = await fetch("/arena/me");
if (!res.ok) throw new Error(res.status);
myArenaData = await res.json();
} catch {
showArenaError("Spielerdaten konnten nicht geladen werden.");
return;
}
}
// Screen wechseln
document.getElementById("arena-mode-screen").style.display = "none";
document.getElementById("arena-2v2-screen").style.display = "flex";
// Lobbyliste anfordern
socket.emit("get_2v2_lobbies");
// Zurück-Button
document.getElementById("arena-2v2-back").onclick = () => {
leave2v2Team(socket);
document.getElementById("arena-2v2-screen").style.display = "none";
document.getElementById("arena-mode-screen").style.display = "";
my2v2TeamId = null;
};
// Team erstellen
document.getElementById("arena-create-team-btn").onclick = () => {
socket.emit("create_2v2_team", myArenaData);
};
// Socket-Listener registrieren (einmalig)
socket.off("2v2_lobbies");
socket.off("2v2_team_joined");
socket.off("2v2_team_update");
socket.off("2v2_partner_left");
socket.off("2v2_searching");
socket.off("match_found_2v2");
socket.off("2v2_error");
socket.on("2v2_lobbies", (list) => render2v2LobbyList(list, socket));
socket.on("2v2_team_joined", (data) => {
my2v2TeamId = data.teamId;
document.getElementById("arena-team-panel").style.display = "block";
document.getElementById("arena-lobby-section").style.display = "none";
});
socket.on("2v2_team_update", (data) => render2v2TeamPanel(data, socket));
socket.on("2v2_partner_left", (data) => {
const status = document.getElementById("arena-team-status");
if (status) status.innerHTML = `<span style="color:#e74c3c;">⚠️ ${data.name} hat das Team verlassen.</span>`;
// Bereit-Button zurücksetzen
render2v2TeamPanel({ teamId: my2v2TeamId, players: [{ name: myArenaData.name, level: myArenaData.level, ready: false }], count: 1 }, socket);
});
socket.on("2v2_searching", () => {
const status = document.getElementById("arena-team-status");
if (status) status.innerHTML = `<div class="arena-searching-box">⏳ Suche nach Gegnerteam…</div>`;
const actions = document.getElementById("arena-team-actions");
if (actions) actions.innerHTML = "";
});
socket.on("match_found_2v2", (data) => {
socket.off("2v2_lobbies");
socket.off("2v2_team_update");
socket.off("2v2_partner_left");
socket.off("2v2_searching");
showMatchFoundOverlay(myArenaData.name, `Team ${data.myTeam === 1 ? 2 : 1}`, () => {
document.getElementById("arena-2v2-screen").style.display = "none";
document.getElementById("arena-mode-screen").style.display = "";
openArenaPopup(
`/arena/2v2?match=${encodeURIComponent(data.matchId)}&slot=${encodeURIComponent(data.mySlot)}`,
data.opponents?.join(" & ") || "Gegner",
data.matchId,
);
});
});
socket.on("2v2_error", (data) => {
const status = document.getElementById("arena-team-status");
if (status) { status.innerHTML = `<span style="color:#e74c3c;">❌ ${data.message}</span>`; }
});
}
function render2v2LobbyList(list, socket) {
const el = document.getElementById("arena-lobby-list");
if (!el) return;
if (!list.length) {
el.innerHTML = `<div class="arena-lobby-empty">Keine offenen Teams vorhanden.</div>`;
return;
}
el.innerHTML = list.map(team => `
<div class="arena-lobby-row">
<div class="arena-lobby-row-info">
<span class="arena-lobby-leader"> ${team.leader}</span>
<span class="arena-lobby-level">Lvl ${team.leaderLevel}</span>
<span class="arena-lobby-count">${team.count}/2 Spieler</span>
</div>
<button class="arena-btn-join" data-teamid="${team.teamId}">Beitreten</button>
</div>
`).join("");
el.querySelectorAll(".arena-btn-join").forEach(btn => {
btn.addEventListener("click", () => {
socket.emit("join_2v2_team", { teamId: btn.dataset.teamid, playerData: myArenaData });
});
});
}
function render2v2TeamPanel(data, socket) {
const playersEl = document.getElementById("arena-team-players");
const actionsEl = document.getElementById("arena-team-actions");
const statusEl = document.getElementById("arena-team-status");
if (!playersEl || !actionsEl) return;
playersEl.innerHTML = data.players.map(p => `
<div class="arena-team-player ${p.ready ? 'ready' : ''}">
<span class="arena-team-player-name">${p.name}</span>
<span class="arena-team-player-level">Lvl ${p.level}</span>
<span class="arena-team-player-status">${p.ready ? '✅ Bereit' : '⌛ Wartet'}</span>
</div>
`).join("");
if (data.count < 2) {
actionsEl.innerHTML = `<div class="arena-waiting-partner">Warte auf Partner…</div>`;
if (statusEl) statusEl.innerHTML = "";
} else {
const myEntry = data.players.find(p => p.name === myArenaData?.name);
const iAmReady = myEntry?.ready;
actionsEl.innerHTML = iAmReady
? `<button class="arena-btn-ready active" disabled>✅ Du bist bereit</button>`
: `<button class="arena-btn-ready" id="arena-ready-btn">⚔️ Bereit</button>`;
if (!iAmReady) {
document.getElementById("arena-ready-btn")?.addEventListener("click", () => {
socket.emit("2v2_player_ready", { teamId: my2v2TeamId });
});
}
}
}
function leave2v2Team(socket) {
if (my2v2TeamId) {
socket.emit("leave_2v2_team");
my2v2TeamId = null;
}
socket.off("2v2_lobbies");
socket.off("2v2_team_joined");
socket.off("2v2_team_update");
socket.off("2v2_partner_left");
socket.off("2v2_searching");
socket.off("match_found_2v2");
socket.off("2v2_error");
}
/* ── 1v1: Hauptlogik ───────────────────────────────────────────────────────── */
async function handle1v1Click(card) {
const socket = getSocket();

View File

@ -202,16 +202,30 @@ export async function loadEvents() {
</div>
</div>
<!-- Arena Daily UI (nur 2v2) -->
<div id="arena2-ui" class="booster-ui" style="display:none;">
<button class="booster-back-btn" id="arena2-back-btn"> Zurück</button>
<div class="arena-daily-wrap">
<div class="arena-mode-card-daily" id="arena-2v2-card">
<div class="arena-mode-icon"></div>
<div class="arena-mode-label-daily">2v2</div>
<div class="arena-mode-desc-daily">Verbünde dich mit einem Kameraden im Kampf</div>
<!-- Arena Daily UI (2v2 Lobby) -->
<div id="arena2-ui" class="booster-ui" style="display:none; flex-direction:column; gap:14px; padding:16px; overflow-y:auto;">
<div style="display:flex; align-items:center; gap:14px;">
<button class="booster-back-btn" id="arena2-back-btn"> Zurück</button>
<span style="font-family:'Cinzel',serif; font-size:14px; color:#f0d060;"> 2v2 Team-Lobby</span>
</div>
<!-- Team-Panel -->
<div id="daily-team-panel" style="display:none;">
<div class="arena-team-box">
<div class="arena-team-title">Dein Team</div>
<div id="daily-team-players"></div>
<div id="daily-team-actions"></div>
</div>
<div id="arena2-daily-status" style="display:none;"></div>
<div id="daily-team-status" style="margin-top:8px;"></div>
</div>
<!-- Lobby-Liste -->
<div id="daily-lobby-section">
<div class="arena-lobby-actions">
<button class="arena-btn-create" id="daily-create-team-btn"> Eigenes Team erstellen</button>
</div>
<div class="arena-lobby-title">Offene Teams</div>
<div id="daily-lobby-list"><div class="arena-lobby-empty">Keine offenen Teams vorhanden.</div></div>
</div>
</div>
@ -258,7 +272,8 @@ export async function loadEvents() {
if (card.dataset.type === "arena2") {
eventsGrid.style.display = "none";
arena2Ui.style.display = "flex";
resetArena2Daily();
daily2v2Leave();
open2v2DailyLobby();
return;
}
if (card.dataset.type === "gold") {
@ -511,110 +526,182 @@ export async function loadEvents() {
}
body.querySelector("#arena2-back-btn").addEventListener("click", () => {
if (isArena2Searching) return;
if (isArena2InMatch) return;
eventsGrid.style.display = "";
arena2Ui.style.display = "none";
cancelArena2Search();
daily2v2Leave();
});
/* ── Arena2 Daily Zustand (2v2) ── */
let isArena2Searching = false;
/* ── Arena2 Daily Zustand (2v2 Lobby) ── */
let isArena2InMatch = false;
let daily2v2TeamId = null;
let daily2v2Me = null;
function resetArena2Daily() {
isArena2Searching = false;
const card = body.querySelector("#arena-2v2-card");
const status = body.querySelector("#arena2-daily-status");
if (card) {
card.classList.remove("searching");
card.querySelector(".arena-mode-label-daily").textContent = "2v2";
card.querySelector(".arena-mode-desc-daily").textContent = "Verbünde dich mit einem Kameraden im Kampf";
}
if (status) status.style.display = "none";
const backBtn = body.querySelector("#arena2-back-btn");
if (backBtn) { backBtn.style.opacity = "1"; backBtn.style.cursor = "pointer"; }
}
function cancelArena2Search() {
async function open2v2DailyLobby() {
const socket = window._socket;
if (socket) {
socket.emit("leave_2v2");
socket.off("match_found_2v2");
socket.off("queue_status_2v2");
}
resetArena2Daily();
}
if (!socket) { showDaily2v2Error("❌ Keine Verbindung zum Server."); return; }
body.querySelector("#arena-2v2-card").addEventListener("click", async () => {
if (isArena2Searching) return;
const socket = window._socket;
if (!socket) {
showArena2Status("❌ Keine Verbindung zum Server.", true);
return;
}
let me;
try {
const res = await fetch("/arena/me");
if (!res.ok) throw new Error(res.status);
me = await res.json();
} catch {
showArena2Status("❌ Spielerdaten konnten nicht geladen werden.", true);
return;
}
isArena2Searching = true;
const card2v2 = body.querySelector("#arena-2v2-card");
card2v2.classList.add("searching");
card2v2.querySelector(".arena-mode-label-daily").textContent = "⏳ Suche…";
card2v2.querySelector(".arena-mode-desc-daily").textContent = "Warte auf Mitspieler & Gegner…";
const backBtn = body.querySelector("#arena2-back-btn");
backBtn.style.opacity = "0.35";
backBtn.style.cursor = "not-allowed";
showArena2Status(`⏳ Suche Gegner (Level ${Math.max(1, me.level - 5)}${me.level + 5})…
<br><span class="arena-cancel-link" id="arena2-cancel-link">Suche abbrechen</span>`);
body.querySelector("#arena2-cancel-link")?.addEventListener("click", () => cancelArena2Search());
socket.off("match_found_2v2");
socket.off("queue_status_2v2");
socket.on("queue_status_2v2", (data) => {
if (data.status === "waiting") {
const pool = data.poolSize ? ` · ${data.poolSize} im Pool` : "";
showArena2Status(`⏳ Suche Gegner (Level ${Math.max(1, me.level - 5)}${me.level + 5})${pool}
<br><span class="arena-cancel-link" id="arena2-cancel-link">Suche abbrechen</span>`);
body.querySelector("#arena2-cancel-link")?.addEventListener("click", () => cancelArena2Search());
if (!daily2v2Me) {
try {
const res = await fetch("/arena/me");
if (!res.ok) throw new Error(res.status);
daily2v2Me = await res.json();
} catch {
showDaily2v2Error("❌ Spielerdaten konnten nicht geladen werden.");
return;
}
}
socket.off("2v2_lobbies");
socket.off("2v2_team_joined");
socket.off("2v2_team_update");
socket.off("2v2_partner_left");
socket.off("2v2_searching");
socket.off("match_found_2v2");
socket.off("2v2_error");
socket.emit("get_2v2_lobbies");
socket.on("2v2_lobbies", (list) => renderDaily2v2Lobbies(list, socket));
socket.on("2v2_team_joined", (data) => {
daily2v2TeamId = data.teamId;
body.querySelector("#daily-team-panel").style.display = "block";
body.querySelector("#daily-lobby-section").style.display = "none";
});
socket.on("2v2_team_update", (data) => renderDaily2v2Team(data, socket));
socket.on("2v2_partner_left", (data) => {
const s = body.querySelector("#daily-team-status");
if (s) s.innerHTML = `<span style="color:#e74c3c;">⚠️ ${data.name} hat das Team verlassen.</span>`;
});
socket.on("2v2_searching", () => {
const s = body.querySelector("#daily-team-status");
if (s) s.innerHTML = `<div class="arena-searching-box">⏳ Suche nach Gegnerteam…</div>`;
const a = body.querySelector("#daily-team-actions");
if (a) a.innerHTML = "";
// Zurück sperren während Suche
const backBtn = body.querySelector("#arena2-back-btn");
if (backBtn) { backBtn.style.opacity = "0.35"; backBtn.style.cursor = "not-allowed"; }
isArena2InMatch = true;
});
socket.once("match_found_2v2", (data) => {
socket.off("queue_status_2v2");
isArena2Searching = false;
socket.off("2v2_lobbies");
socket.off("2v2_team_update");
socket.off("2v2_partner_left");
socket.off("2v2_searching");
isArena2InMatch = false;
markDailyComplete(3);
showArenaMatchFound(me.name, data.opponent.name, () => {
showArenaMatchFound(daily2v2Me.name, `Team ${data.myTeam === 1 ? 2 : 1}`, () => {
eventsGrid.style.display = "";
arena2Ui.style.display = "none";
openArenaMatchPopup(
`/arena/2v2?match=${encodeURIComponent(data.matchId)}&slot=${encodeURIComponent(data.mySlot)}`,
data.opponent.name,
data.opponents?.join(" & ") || "Gegner",
data.matchId,
"2v2",
);
});
});
socket.emit("join_2v2", { id: me.id, name: me.name, level: me.level });
});
socket.on("2v2_error", (data) => showDaily2v2Error(`${data.message}`));
function showArena2Status(html, isError = false) {
const box = body.querySelector("#arena2-daily-status");
if (!box) return;
box.style.display = "block";
box.style.color = isError ? "#e74c3c" : "#dceb15";
box.innerHTML = html;
if (isError) setTimeout(() => { box.style.display = "none"; }, 3000);
// Buttons
body.querySelector("#daily-create-team-btn").onclick = () => {
socket.emit("create_2v2_team", daily2v2Me);
};
}
function renderDaily2v2Lobbies(list, socket) {
const el = body.querySelector("#daily-lobby-list");
if (!el) return;
if (!list.length) {
el.innerHTML = `<div class="arena-lobby-empty">Keine offenen Teams vorhanden.</div>`;
return;
}
el.innerHTML = list.map(t => `
<div class="arena-lobby-row">
<div class="arena-lobby-row-info">
<span class="arena-lobby-leader"> ${t.leader}</span>
<span class="arena-lobby-level">Lvl ${t.leaderLevel}</span>
<span class="arena-lobby-count">${t.count}/2</span>
</div>
<button class="arena-btn-join" data-teamid="${t.teamId}">Beitreten</button>
</div>`).join("");
el.querySelectorAll(".arena-btn-join").forEach(btn => {
btn.addEventListener("click", () => {
socket.emit("join_2v2_team", { teamId: btn.dataset.teamid, playerData: daily2v2Me });
});
});
}
function renderDaily2v2Team(data, socket) {
const playersEl = body.querySelector("#daily-team-players");
const actionsEl = body.querySelector("#daily-team-actions");
if (!playersEl || !actionsEl) return;
playersEl.innerHTML = data.players.map(p => `
<div class="arena-team-player ${p.ready ? 'ready' : ''}">
<span class="arena-team-player-name">${p.name}</span>
<span class="arena-team-player-level">Lvl ${p.level}</span>
<span class="arena-team-player-status">${p.ready ? '✅ Bereit' : '⌛ Wartet'}</span>
</div>`).join("");
if (data.count < 2) {
actionsEl.innerHTML = `<div class="arena-waiting-partner">Warte auf Partner…</div>`;
} else {
const myEntry = data.players.find(p => p.name === daily2v2Me?.name);
const iAmReady = myEntry?.ready;
actionsEl.innerHTML = iAmReady
? `<button class="arena-btn-ready active" disabled>✅ Du bist bereit</button>`
: `<button class="arena-btn-ready" id="daily-ready-btn">⚔️ Bereit</button>`;
if (!iAmReady) {
body.querySelector("#daily-ready-btn")?.addEventListener("click", () => {
socket.emit("2v2_player_ready", { teamId: daily2v2TeamId });
});
}
}
}
function daily2v2Leave() {
const socket = window._socket;
if (socket) {
socket.emit("leave_2v2_team");
socket.off("2v2_lobbies");
socket.off("2v2_team_joined");
socket.off("2v2_team_update");
socket.off("2v2_partner_left");
socket.off("2v2_searching");
socket.off("match_found_2v2");
socket.off("2v2_error");
}
daily2v2TeamId = null;
isArena2InMatch = false;
// Reset UI
const panel = body.querySelector("#daily-team-panel");
const section = body.querySelector("#daily-lobby-section");
const backBtn = body.querySelector("#arena2-back-btn");
if (panel) panel.style.display = "none";
if (section) section.style.display = "";
if (backBtn) { backBtn.style.opacity = "1"; backBtn.style.cursor = "pointer"; }
}
function showDaily2v2Error(msg) {
const s = body.querySelector("#daily-team-status");
if (!s) return;
s.innerHTML = `<span style="color:#e74c3c;">${msg}</span>`;
setTimeout(() => { s.innerHTML = ""; }, 3000);
}
// Lobby beim Öffnen des Arena2-UI initialisieren
const origArena2Click = body.querySelector('.event-card[data-type="arena2"]');
if (origArena2Click) {
origArena2Click.addEventListener("click", () => {
// open2v2DailyLobby wird nach dem UI-Wechsel in resetArena2Daily aufgerufen
});
}
/* ── Booster Zustand ── */

572
sockets/arena_socket.js Normal file
View File

@ -0,0 +1,572 @@
/* ============================================================
sockets/arena.js
Alle Socket-Events rund um 1v1 Matchmaking, Spielfeld & Bereit-System
============================================================ */
const waitingPool = new Map(); // socketId → { socket, player }
const LEVEL_RANGE = 5;
// 2v2 Team-Lobby
// teams2v2: teamId → { id, players: [{socketId, player}], ready: Set<socketId> }
const teams2v2 = new Map();
const readyTeams = new Map(); // teamId → team (fertige Teams warten auf Matchmaking)
function generateId() {
return `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
}
/* ── 2v2 Lobby-Liste an alle schicken ── */
function broadcastLobbies(io) {
const list = [];
for (const [teamId, team] of teams2v2) {
if (team.players.length < 2) {
list.push({
teamId,
leader: team.players[0]?.player?.name || "?",
leaderLevel: team.players[0]?.player?.level || 1,
count: team.players.length,
});
}
}
io.emit("2v2_lobbies", list);
}
/* ── 2v2 Team-Status an Teammitglieder senden ── */
function broadcastTeamStatus(io, teamId) {
const team = teams2v2.get(teamId);
if (!team) return;
const data = {
teamId,
players: team.players.map(p => ({
name: p.player.name,
level: p.player.level,
ready: team.ready.has(p.socketId),
})),
count: team.players.length,
};
for (const p of team.players) {
io.to(p.socketId).emit("2v2_team_update", data);
}
}
/* ── 2v2 Matchmaking ── */
function tryMatchmaking2v2(io) {
const readyList = Array.from(readyTeams.values());
if (readyList.length < 2) return;
const team1 = readyList[0];
const team2 = readyList[1];
readyTeams.delete(team1.id);
readyTeams.delete(team2.id);
teams2v2.delete(team1.id);
teams2v2.delete(team2.id);
const matchId = `2v2_${generateId()}`;
const allPlayers = [
{ team: 1, ...team1.players[0] },
{ team: 1, ...team1.players[1] },
{ team: 2, ...team2.players[0] },
{ team: 2, ...team2.players[1] },
];
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);
io.to(p.socketId).emit("match_found_2v2", {
matchId,
myTeam: p.team,
teammates,
opponents,
mySlot: `team${p.team}_player${team1.players.indexOf(p) >= 0 ? team1.players.findIndex(x=>x.socketId===p.socketId)+1 : team2.players.findIndex(x=>x.socketId===p.socketId)+1}`,
});
}
console.log(`[2v2] Match: Team1(${team1.players.map(p=>p.player.name)}) vs Team2(${team2.players.map(p=>p.player.name)}) | ${matchId}`);
}
// Werden beim ersten Event lazy initialisiert (auf io gespeichert)
// io._arenaRooms → matchId → { sockets, names }
// io._arenaReady → matchId → Set of ready slots
// io._arenaTimers → matchId → intervalId
const READY_TIMEOUT = 30; // Sekunden bis Match abgebrochen wird
function startReadyTimer(io, matchId) {
if (!io._arenaTimers) io._arenaTimers = new Map();
if (io._arenaTimers.has(matchId)) return;
let remaining = READY_TIMEOUT;
io.to("arena_" + matchId).emit("ready_timer", { remaining });
const interval = setInterval(() => {
remaining--;
io.to("arena_" + matchId).emit("ready_timer", { remaining });
if (remaining <= 0) {
clearInterval(interval);
io._arenaTimers.delete(matchId);
console.log(`[1v1] Match ${matchId} abgebrochen Zeit abgelaufen.`);
io.to("arena_" + matchId).emit("match_cancelled", {
reason: "timeout",
message: "Zeit abgelaufen Match wird abgebrochen.",
});
}
}, 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);
console.log(`[1v1] Timer für Match ${matchId} gestoppt (beide bereit).`);
}
}
function tryMatchmaking(io, newSocketId) {
const challenger = waitingPool.get(newSocketId);
if (!challenger) return;
for (const [id, entry] of waitingPool) {
if (id === newSocketId) continue;
const levelDiff = Math.abs(entry.player.level - challenger.player.level);
if (levelDiff <= LEVEL_RANGE) {
waitingPool.delete(newSocketId);
waitingPool.delete(id);
const matchId = `match_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
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] Match: ${challenger.player.name} (Lvl ${challenger.player.level})` +
` vs ${entry.player.name} (Lvl ${entry.player.level}) | ${matchId}`
);
return;
}
}
}
function registerArenaHandlers(io, socket) {
/* ── 1v1: Queue beitreten ── */
socket.on("join_1v1", (playerData) => {
if (waitingPool.has(socket.id)) return;
const player = {
id: playerData.id,
name: playerData.name,
level: Number(playerData.level) || 1,
};
waitingPool.set(socket.id, { socket, player });
socket.emit("queue_status", {
status: "waiting",
poolSize: waitingPool.size,
message: `Suche Gegner (Level ${player.level - LEVEL_RANGE}${player.level + LEVEL_RANGE})…`,
});
console.log(`[1v1] ${player.name} (Lvl ${player.level}) im Pool. Größe: ${waitingPool.size}`);
tryMatchmaking(io, socket.id);
});
/* ── 1v1: Queue verlassen ── */
socket.on("leave_1v1", () => {
if (waitingPool.delete(socket.id)) {
socket.emit("queue_status", { status: "left" });
console.log(`[1v1] ${socket.id} hat Pool verlassen.`);
}
});
/*
2v2 TEAM LOBBY
*/
/* ── Lobby-Liste anfordern ── */
socket.on("get_2v2_lobbies", () => {
const list = [];
for (const [teamId, team] of teams2v2) {
if (team.players.length < 2) {
list.push({
teamId,
leader: team.players[0]?.player?.name || "?",
leaderLevel: team.players[0]?.player?.level || 1,
count: team.players.length,
});
}
}
socket.emit("2v2_lobbies", list);
});
/* ── Neues Team erstellen ── */
socket.on("create_2v2_team", (playerData) => {
// Erst aus alten Teams entfernen
leaveAllTeams(socket.id, io);
const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1 };
const teamId = `team_${generateId()}`;
teams2v2.set(teamId, {
id: teamId,
players: [{ socketId: socket.id, player }],
ready: new Set(),
});
socket.emit("2v2_team_joined", { teamId, isLeader: true });
broadcastTeamStatus(io, teamId);
broadcastLobbies(io);
console.log(`[2v2] Team ${teamId} erstellt von ${player.name}`);
});
/* ── Team beitreten ── */
socket.on("join_2v2_team", (data) => {
const { teamId, playerData } = data;
const team = teams2v2.get(teamId);
if (!team) return socket.emit("2v2_error", { message: "Team nicht mehr verfügbar." });
if (team.players.length >= 2) return socket.emit("2v2_error", { message: "Team ist bereits voll." });
if (team.players.find(p => p.socketId === socket.id)) return;
leaveAllTeams(socket.id, io);
const player = { id: playerData.id, name: playerData.name, level: Number(playerData.level) || 1 };
team.players.push({ socketId: socket.id, player });
socket.emit("2v2_team_joined", { teamId, isLeader: false });
broadcastTeamStatus(io, teamId);
broadcastLobbies(io); // Team aus offener Liste entfernen
console.log(`[2v2] ${player.name} hat Team ${teamId} beigetreten`);
});
/* ── Team verlassen ── */
socket.on("leave_2v2_team", () => {
leaveAllTeams(socket.id, io);
});
/* ── Bereit klicken ── */
socket.on("2v2_player_ready", (data) => {
const { teamId } = data;
const team = teams2v2.get(teamId);
if (!team || team.players.length < 2) return;
team.ready.add(socket.id);
broadcastTeamStatus(io, teamId);
console.log(`[2v2] Bereit in Team ${teamId}: ${team.ready.size}/2`);
// Beide bereit → Team in Matchmaking-Pool
if (team.ready.size >= 2) {
readyTeams.set(teamId, team);
for (const p of team.players) {
io.to(p.socketId).emit("2v2_searching", { message: "Suche nach Gegnerteam…" });
}
console.log(`[2v2] Team ${teamId} sucht Gegner. Ready-Teams: ${readyTeams.size}`);
tryMatchmaking2v2(io);
}
});
/* ── Spielfeld: Spieler betritt Arena-Room ── */
socket.on("arena_join", (data) => {
const { matchId, slot } = data;
if (!matchId || !slot) return;
if (!io._arenaRooms) io._arenaRooms = new Map();
if (!io._arenaRooms.has(matchId)) {
io._arenaRooms.set(matchId, { sockets: {}, names: {} });
}
const room = io._arenaRooms.get(matchId);
room.sockets[slot] = socket.id;
room.names[slot] = socket.user || "Spieler";
socket.join("arena_" + matchId);
const otherSlot = slot === "player1" ? "player2" : "player1";
if (room.sockets[otherSlot]) {
io.to("arena_" + matchId).emit("arena_ready", {
player1: room.names["player1"] || "Spieler 1",
player2: room.names["player2"] || "Spieler 2",
});
console.log(`[Arena] Match ${matchId} bereit: ${room.names["player1"]} vs ${room.names["player2"]}`);
startReadyTimer(io, matchId);
} else {
socket.to("arena_" + matchId).emit("arena_opponent_joined", {
name: room.names[slot],
slot,
});
}
});
/* ── Bereit-System ── */
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);
io.to("arena_" + matchId).emit("ready_status", {
readyCount: readySet.size,
readySlots: Array.from(readySet),
});
console.log(`[1v1] ${slot} bereit in ${matchId} (${readySet.size}/2)`);
if (readySet.size >= 2) {
stopReadyTimer(io, matchId);
io._arenaReady.delete(matchId);
}
});
/* ── Aufgeben ── */
socket.on("player_surrender", (data) => {
const { matchId, slot } = data;
console.log(`[1v1] ${slot} hat aufgegeben in Match ${matchId}`);
io.to("arena_" + matchId).emit("player_surrendered", { slot });
});
/* ── Disconnect ── */
socket.on("disconnect", () => {
if (waitingPool.delete(socket.id)) {
console.log(`[1v1] ${socket.id} disconnected aus 1v1-Pool entfernt.`);
}
leaveAllTeams(socket.id, io);
});
}
/* ── Hilfsfunktion: Spieler aus allen 2v2-Teams entfernen ── */
function leaveAllTeams(socketId, io) {
for (const [teamId, team] of teams2v2) {
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);
readyTeams.delete(teamId);
if (team.players.length === 0) {
teams2v2.delete(teamId);
console.log(`[2v2] Team ${teamId} aufgelöst.`);
} else {
// Verbleibenden Spieler informieren
broadcastTeamStatus(io, teamId);
for (const p of team.players) {
io.to(p.socketId).emit("2v2_partner_left", { name: playerName });
}
console.log(`[2v2] ${playerName} hat Team ${teamId} verlassen.`);
}
broadcastLobbies(io);
break;
}
}
module.exports = { registerArenaHandlers };
function startReadyTimer(io, matchId) {
if (!io._arenaTimers) io._arenaTimers = new Map();
if (io._arenaTimers.has(matchId)) return; // läuft bereits
let remaining = READY_TIMEOUT;
// Sofort ersten Tick senden
io.to("arena_" + matchId).emit("ready_timer", { remaining });
const interval = setInterval(() => {
remaining--;
io.to("arena_" + matchId).emit("ready_timer", { remaining });
if (remaining <= 0) {
clearInterval(interval);
io._arenaTimers.delete(matchId);
// Match abbrechen Funktion noch offen
console.log(`[1v1] Match ${matchId} abgebrochen Zeit abgelaufen.`);
io.to("arena_" + matchId).emit("match_cancelled", {
reason: "timeout",
message: "Zeit abgelaufen Match wird abgebrochen.",
});
}
}, 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);
console.log(`[1v1] Timer für Match ${matchId} gestoppt (beide bereit).`);
}
}
function tryMatchmaking(io, newSocketId) {
const challenger = waitingPool.get(newSocketId);
if (!challenger) return;
for (const [id, entry] of waitingPool) {
if (id === newSocketId) continue;
const levelDiff = Math.abs(entry.player.level - challenger.player.level);
if (levelDiff <= LEVEL_RANGE) {
waitingPool.delete(newSocketId);
waitingPool.delete(id);
const matchId = `match_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
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] Match: ${challenger.player.name} (Lvl ${challenger.player.level})` +
` vs ${entry.player.name} (Lvl ${entry.player.level}) | ${matchId}`
);
return;
}
}
}
function registerArenaHandlers(io, socket) {
/* ── Queue beitreten ── */
socket.on("join_1v1", (playerData) => {
if (waitingPool.has(socket.id)) return;
const player = {
id: playerData.id,
name: playerData.name,
level: Number(playerData.level) || 1,
};
waitingPool.set(socket.id, { socket, player });
socket.emit("queue_status", {
status: "waiting",
poolSize: waitingPool.size,
message: `Suche Gegner (Level ${player.level - LEVEL_RANGE}${player.level + LEVEL_RANGE})…`,
});
console.log(`[1v1] ${player.name} (Lvl ${player.level}) im Pool. Größe: ${waitingPool.size}`);
tryMatchmaking(io, socket.id);
});
/* ── Queue verlassen ── */
socket.on("leave_1v1", () => {
if (waitingPool.delete(socket.id)) {
socket.emit("queue_status", { status: "left" });
console.log(`[1v1] ${socket.id} hat Pool verlassen.`);
}
});
/* ── Spielfeld: Spieler betritt Arena-Room ── */
socket.on("arena_join", (data) => {
const { matchId, slot } = data;
if (!matchId || !slot) return;
if (!io._arenaRooms) io._arenaRooms = new Map();
if (!io._arenaRooms.has(matchId)) {
io._arenaRooms.set(matchId, { sockets: {}, names: {} });
}
const room = io._arenaRooms.get(matchId);
room.sockets[slot] = socket.id;
room.names[slot] = socket.user || "Spieler";
socket.join("arena_" + matchId);
const otherSlot = slot === "player1" ? "player2" : "player1";
if (room.sockets[otherSlot]) {
io.to("arena_" + matchId).emit("arena_ready", {
player1: room.names["player1"] || "Spieler 1",
player2: room.names["player2"] || "Spieler 2",
});
console.log(`[Arena] Match ${matchId} bereit: ${room.names["player1"]} vs ${room.names["player2"]}`);
// 30-Sekunden Bereit-Timer starten
startReadyTimer(io, matchId);
} else {
socket.to("arena_" + matchId).emit("arena_opponent_joined", {
name: room.names[slot],
slot,
});
}
});
/* ── Bereit-System ── */
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);
io.to("arena_" + matchId).emit("ready_status", {
readyCount: readySet.size,
readySlots: Array.from(readySet),
});
console.log(`[1v1] ${slot} bereit in ${matchId} (${readySet.size}/2)`);
if (readySet.size >= 2) {
stopReadyTimer(io, matchId);
io._arenaReady.delete(matchId);
}
});
/* ── Aufgeben ── */
socket.on("player_surrender", (data) => {
const { matchId, slot } = data;
console.log(`[1v1] ${slot} hat aufgegeben in Match ${matchId}`);
// Aufgabe-Logik kommt hier rein
io.to("arena_" + matchId).emit("player_surrendered", { slot });
});
/* ── Disconnect: aus Pool entfernen ── */
socket.on("disconnect", () => {
if (waitingPool.delete(socket.id)) {
console.log(`[1v1] ${socket.id} disconnected aus Pool entfernt.`);
}
});
}
module.exports = { registerArenaHandlers };