xfghmxf
This commit is contained in:
parent
a766fc7fe9
commit
65b0d29595
23
app.js
23
app.js
@ -24,12 +24,12 @@ const blackmarket = require("./routes/blackmarket.route");
|
||||
const mineRoute = require("./routes/mine.route");
|
||||
const carddeckRoutes = require("./routes/carddeck.route");
|
||||
const arenaRoutes = require("./routes/arena.route");
|
||||
const { registerArenaHandlers } = require("./sockets/arena");
|
||||
const { registerArenaHandlers } = require("./sockets/arena.socket");
|
||||
const { registerChatHandlers } = require("./sockets/chat");
|
||||
const boosterRoutes = require("./routes/booster.route");
|
||||
const pointsRoutes = require("./routes/points.route");
|
||||
const pointsRoutes = require("./routes/points.route");
|
||||
const combineRoutes = require("./routes/combine.route");
|
||||
const bazaarRoutes = require("./routes/bazaar.route");
|
||||
const bazaarRoutes = require("./routes/bazaar.route");
|
||||
|
||||
const compression = require("compression");
|
||||
|
||||
@ -61,11 +61,20 @@ app.use(
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrcAttr: ["'unsafe-inline'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com"],
|
||||
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||
styleSrc: [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
"https://fonts.googleapis.com",
|
||||
"https://cdnjs.cloudflare.com",
|
||||
],
|
||||
fontSrc: [
|
||||
"'self'",
|
||||
"https://fonts.gstatic.com",
|
||||
"https://cdnjs.cloudflare.com",
|
||||
],
|
||||
imgSrc: ["'self'", "data:", "blob:"],
|
||||
connectSrc: ["'self'", "ws:", "wss:"],
|
||||
frameAncestors: ["'self'"], // Erlaubt iframe von eigener Domain
|
||||
frameAncestors: ["'self'"], // Erlaubt iframe von eigener Domain
|
||||
},
|
||||
},
|
||||
}),
|
||||
@ -105,7 +114,7 @@ app.use(
|
||||
app.set("view engine", "ejs");
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
|
||||
const shopRoutes = require("./routes/shop.route");
|
||||
const shopRoutes = require("./routes/shop.route");
|
||||
|
||||
/* ========================
|
||||
WICHTIG: Shop/Webhook VOR express.json()
|
||||
|
||||
199
sockets/arena.js
199
sockets/arena.js
@ -1,199 +0,0 @@
|
||||
/* ============================================================
|
||||
sockets/arena.js
|
||||
Alle Socket-Events rund um 1v1 Matchmaking, Spielfeld & Bereit-System
|
||||
============================================================ */
|
||||
|
||||
const waitingPool = new Map(); // socketId → { socket, player }
|
||||
const LEVEL_RANGE = 5;
|
||||
|
||||
// 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; // 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 };
|
||||
@ -367,7 +367,10 @@ function getRoom(io, matchId) {
|
||||
// Sendet an beide Spieler über ihre gespeicherten Socket-IDs
|
||||
function emitToMatch(io, matchId, event, data) {
|
||||
const room = getRoom(io, matchId);
|
||||
if (!room) { console.warn(`[emitToMatch] Kein Room für ${matchId}`); return; }
|
||||
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);
|
||||
@ -442,7 +445,9 @@ function registerArenaHandlers(io, socket) {
|
||||
}
|
||||
// Sonst: bereits guter Name gespeichert → nicht überschreiben
|
||||
|
||||
console.log(`[1v1] Name gesetzt: slot=${slot}, name=${room.names[slot]}, playerName=${playerName}`);
|
||||
console.log(
|
||||
`[1v1] Name gesetzt: slot=${slot}, name=${room.names[slot]}, playerName=${playerName}`,
|
||||
);
|
||||
|
||||
socket.join("arena_" + matchId);
|
||||
|
||||
@ -479,26 +484,23 @@ function registerArenaHandlers(io, socket) {
|
||||
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) };
|
||||
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}`);
|
||||
console.log(
|
||||
`[1v1] ready_status: ${readySet.size}/2 bereit | Match ${matchId}`,
|
||||
);
|
||||
|
||||
if (readySet.size >= 2) {
|
||||
stopReadyTimer(io, matchId);
|
||||
io._arenaReady.delete(matchId);
|
||||
|
||||
// Server startet Zug direkt – kein end_turn_init vom Client nötig
|
||||
// Startspieler: player1 (deterministisch, kein Flip nötig auf Server-Seite)
|
||||
const starterSlot = "player1";
|
||||
if (!io._turnInit) io._turnInit = new Set();
|
||||
if (!io._turnInit.has(matchId)) {
|
||||
io._turnInit.add(matchId);
|
||||
setTimeout(() => io._turnInit?.delete(matchId), 60000);
|
||||
const room = io._arenaRooms?.get(matchId);
|
||||
const boardCards = room?.boardCards || [];
|
||||
emitToMatch(io, matchId, "turn_change", { activeSlot: starterSlot, boardSync: boardCards });
|
||||
console.log(`[1v1] Spiel startet → ${starterSlot} beginnt | Match ${matchId}`);
|
||||
}
|
||||
// Startspieler wird vom Client per start_turn_request gesendet
|
||||
// (Client kennt durch seed-basiertes Flip wer links = wer anfängt)
|
||||
console.log(
|
||||
`[1v1] Beide bereit – warte auf start_turn_request | Match ${matchId}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -522,7 +524,9 @@ function registerArenaHandlers(io, socket) {
|
||||
boardSync: boardCards,
|
||||
});
|
||||
|
||||
console.log(`[1v1] Zug: ${slot} → ${nextSlot} | boardCards=${boardCards.length} | Match ${matchId}`);
|
||||
console.log(
|
||||
`[1v1] Zug: ${slot} → ${nextSlot} | boardCards=${boardCards.length} | Match ${matchId}`,
|
||||
);
|
||||
});
|
||||
|
||||
/* ── Karte gespielt → direkt an Gegner senden ── */
|
||||
@ -540,20 +544,29 @@ function registerArenaHandlers(io, socket) {
|
||||
roomState.boardCards.push(data);
|
||||
}
|
||||
|
||||
console.log(`[1v1] card_played: ${data.card?.name} → ${data.boardSlot} | Match ${matchId}`);
|
||||
console.log(
|
||||
`[1v1] card_played: ${data.card?.name} → ${data.boardSlot} | Match ${matchId}`,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("end_turn_init", (data) => {
|
||||
// Client sendet nach ready_status den leftSlot (wer anfängt)
|
||||
socket.on("start_turn_request", (data) => {
|
||||
const { matchId, starterSlot } = data;
|
||||
if (!matchId || !starterSlot) return;
|
||||
// Nur einmal senden (erster Empfang gewinnt)
|
||||
if (!io._turnInit) io._turnInit = new Set();
|
||||
// Nur einmal pro Match ausführen
|
||||
if (io._turnInit.has(matchId)) return;
|
||||
io._turnInit.add(matchId);
|
||||
emitToMatch(io, matchId, "turn_change", { activeSlot: starterSlot });
|
||||
console.log(`[1v1] Startzug: ${starterSlot} | Match ${matchId}`);
|
||||
// Cleanup nach 60s (lang genug damit doppelte end_turn_init geblockt bleiben)
|
||||
setTimeout(() => io._turnInit?.delete(matchId), 60000);
|
||||
const room = io._arenaRooms?.get(matchId);
|
||||
const boardCards = room?.boardCards || [];
|
||||
emitToMatch(io, matchId, "turn_change", {
|
||||
activeSlot: starterSlot,
|
||||
boardSync: boardCards,
|
||||
});
|
||||
console.log(
|
||||
`[1v1] Spiel startet → ${starterSlot} (linker Spieler) beginnt | Match ${matchId}`,
|
||||
);
|
||||
});
|
||||
|
||||
/* ── 2v2 & 4v4 ── */
|
||||
@ -167,7 +167,8 @@
|
||||
}
|
||||
|
||||
/* ── Reichweite & Laufen Badges (Hand + Board) ── */
|
||||
.cs-range, .cs-race {
|
||||
.cs-range,
|
||||
.cs-race {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -182,14 +183,16 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
.cs-range {
|
||||
bottom: 22px; left: 3px;
|
||||
background: rgba(30,20,0,0.85);
|
||||
bottom: 22px;
|
||||
left: 3px;
|
||||
background: rgba(30, 20, 0, 0.85);
|
||||
border: 1px solid #e8b84b;
|
||||
color: #e8b84b;
|
||||
}
|
||||
.cs-race {
|
||||
bottom: 6px; left: 3px;
|
||||
background: rgba(0,25,0,0.85);
|
||||
bottom: 6px;
|
||||
left: 3px;
|
||||
background: rgba(0, 25, 0, 0.85);
|
||||
border: 1px solid #7de87d;
|
||||
color: #7de87d;
|
||||
}
|
||||
@ -416,7 +419,7 @@
|
||||
|
||||
/* ── Stat-Icon SVGs ─────────────────────────────────── */
|
||||
const SVG_RANGE = `<svg viewBox="0 0 16 16" width="10" height="10" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#e8b84b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2 Q1 8 4 14"/><line x1="4" y1="2" x2="4" y2="14" stroke-width="0.7" stroke-dasharray="2,1.5"/><line x1="4" y1="8" x2="13" y2="8"/><polyline points="11,6 13,8 11,10"/><line x1="5" y1="7" x2="4" y2="8"/><line x1="5" y1="9" x2="4" y2="8"/></svg>`;
|
||||
const SVG_RACE = `<svg viewBox="0 0 16 16" width="10" height="10" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#7de87d" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="2.5" r="1.4" fill="#7de87d" stroke="none"/><line x1="9" y1="4" x2="8" y2="9"/><line x1="8" y1="9" x2="10" y2="14"/><line x1="8" y1="9" x2="6" y2="13"/><line x1="8.5" y1="5.5" x2="11" y2="8"/><line x1="8.5" y1="5.5" x2="6" y2="7"/></svg>`;
|
||||
const SVG_RACE = `<svg viewBox="0 0 16 16" width="10" height="10" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#7de87d" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="2.5" r="1.4" fill="#7de87d" stroke="none"/><line x1="9" y1="4" x2="8" y2="9"/><line x1="8" y1="9" x2="10" y2="14"/><line x1="8" y1="9" x2="6" y2="13"/><line x1="8.5" y1="5.5" x2="11" y2="8"/><line x1="8.5" y1="5.5" x2="6" y2="7"/></svg>`;
|
||||
|
||||
/* ── Slot rendern ──────────────────────────────────── */
|
||||
function renderHandSlot(id) {
|
||||
@ -437,8 +440,8 @@
|
||||
|
||||
const atkVal = card.attack ?? null;
|
||||
const defVal = card.defends ?? null;
|
||||
const rngVal = card.range ?? null;
|
||||
const rceVal = card.race ?? null;
|
||||
const rngVal = card.range ?? null;
|
||||
const rceVal = card.race ?? null;
|
||||
|
||||
const statsHtml = `
|
||||
<div class="card-stat-overlay">
|
||||
@ -450,12 +453,13 @@
|
||||
: ""
|
||||
}
|
||||
${rngVal != null ? `<span class="cs-range">${SVG_RANGE} ${rngVal}</span>` : ""}
|
||||
${rceVal != null ? `<span class="cs-race">${SVG_RACE} ${rceVal}</span>` : ""}
|
||||
${rceVal != null ? `<span class="cs-race">${SVG_RACE} ${rceVal}</span>` : ""}
|
||||
</div>`;
|
||||
|
||||
const readyBadge = (isReady && isMyTurn)
|
||||
? `<div class="hand-slot-ready-badge">SPIELEN</div>`
|
||||
: "";
|
||||
const readyBadge =
|
||||
isReady && isMyTurn
|
||||
? `<div class="hand-slot-ready-badge">SPIELEN</div>`
|
||||
: "";
|
||||
|
||||
slot.innerHTML = card.image
|
||||
? `<img src="/images/cards/${card.image}"
|
||||
@ -547,41 +551,33 @@
|
||||
})();
|
||||
|
||||
/* ══════════════════════════════════════════════════════
|
||||
ZUG-TIMER (20 Sekunden)
|
||||
ZUG-TIMER (30 Sekunden) – läuft für BEIDE Spieler sichtbar
|
||||
══════════════════════════════════════════════════════ */
|
||||
const TURN_SECONDS = 20;
|
||||
const TURN_SECONDS = 30;
|
||||
const TT_CIRCUM = 2 * Math.PI * 18; // r=18
|
||||
let turnTimerInt = null;
|
||||
let turnSecsLeft = TURN_SECONDS;
|
||||
|
||||
function startTurnTimer() {
|
||||
// activeName: Name des Spielers der gerade dran ist
|
||||
function startTurnTimer(activeName) {
|
||||
clearInterval(turnTimerInt);
|
||||
turnSecsLeft = TURN_SECONDS;
|
||||
updateTimerUI(turnSecsLeft);
|
||||
updateTimerUI(turnSecsLeft, activeName);
|
||||
|
||||
const wrap = document.getElementById("turn-timer-wrap");
|
||||
if (wrap) wrap.style.display = "flex";
|
||||
|
||||
turnTimerInt = setInterval(() => {
|
||||
turnSecsLeft--;
|
||||
updateTimerUI(turnSecsLeft);
|
||||
updateTimerUI(turnSecsLeft, activeName);
|
||||
if (turnSecsLeft <= 0) {
|
||||
clearInterval(turnTimerInt);
|
||||
// Nur der aktive Spieler sendet end_turn
|
||||
if (isMyTurn) {
|
||||
tickHandCooldowns();
|
||||
drawNextCard(); // eine Karte aus dem Deck nachziehen
|
||||
drawNextCard();
|
||||
setTurnState(false);
|
||||
socket.emit("end_turn", { matchId, slot: mySlot });
|
||||
|
||||
// Watchdog: falls turn_change nach 5s nicht ankommt → selbst freischalten
|
||||
setTimeout(() => {
|
||||
if (!isMyTurn) {
|
||||
console.warn(
|
||||
"[1v1] Kein turn_change nach 5s – Zug lokal übergeben",
|
||||
);
|
||||
setTurnState(true);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
@ -593,15 +589,17 @@
|
||||
if (wrap) wrap.style.display = "none";
|
||||
}
|
||||
|
||||
function updateTimerUI(secs) {
|
||||
function updateTimerUI(secs, activeName) {
|
||||
const num = document.getElementById("turn-timer-num");
|
||||
const circle = document.getElementById("tt-circle");
|
||||
const wrap = document.getElementById("turn-timer-wrap");
|
||||
if (num) num.textContent = secs;
|
||||
if (wrap && activeName) wrap.title = activeName + " ist am Zug";
|
||||
if (circle) {
|
||||
circle.style.strokeDashoffset = TT_CIRCUM * (1 - secs / TURN_SECONDS);
|
||||
circle.style.stroke =
|
||||
secs > 10 ? "#27ae60" : secs > 5 ? "#f39c12" : "#e74c3c";
|
||||
if (secs <= 5) {
|
||||
secs > 15 ? "#27ae60" : secs > 8 ? "#f39c12" : "#e74c3c";
|
||||
if (secs <= 8) {
|
||||
num.style.color = "#e74c3c";
|
||||
num.style.animation = "tt-pulse 0.5s ease-in-out infinite";
|
||||
} else {
|
||||
@ -619,7 +617,8 @@
|
||||
let amILeftPlayer = null;
|
||||
let isMyTurn = false;
|
||||
|
||||
function setTurnState(myTurn) {
|
||||
// activeName = wer gerade dran ist (für Timer-Text + Indicator)
|
||||
function setTurnState(myTurn, activeName) {
|
||||
isMyTurn = myTurn;
|
||||
const btn = document.getElementById("end-turn-btn");
|
||||
if (!btn) return;
|
||||
@ -628,36 +627,29 @@
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Zug beenden";
|
||||
btn.style.opacity = "1";
|
||||
// Draggable für bereite Karten aktivieren
|
||||
setTimeout(() => handCardIds.forEach(id => renderHandSlot(id)), 50);
|
||||
document.getElementById("turn-indicator")?.remove();
|
||||
startTurnTimer();
|
||||
// Watchdog für wartenden Spieler abbrechen falls vorhanden
|
||||
if (window._waitWatchdog) {
|
||||
clearTimeout(window._waitWatchdog);
|
||||
window._waitWatchdog = null;
|
||||
}
|
||||
// Draggable für bereite Karten aktivieren
|
||||
setTimeout(() => handCardIds.forEach((id) => renderHandSlot(id)), 50);
|
||||
// Timer für beide Spieler sichtbar starten
|
||||
startTurnTimer(activeName || "Du");
|
||||
} else {
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = "0.4";
|
||||
stopTurnTimer();
|
||||
// Draggable deaktivieren
|
||||
handCardIds.forEach(id => { const s = document.getElementById(id); if(s){s.draggable=false;delete s.dataset.cardSlotId;} });
|
||||
showTurnIndicator();
|
||||
// Watchdog: falls nach 26s kein turn_change → eigenen Zug erzwingen
|
||||
if (window._waitWatchdog) clearTimeout(window._waitWatchdog);
|
||||
window._waitWatchdog = setTimeout(() => {
|
||||
if (!isMyTurn) {
|
||||
console.warn(
|
||||
"[1v1] Kein turn_change nach 26s – Zug lokal übernommen",
|
||||
);
|
||||
setTurnState(true);
|
||||
// Draggable sofort deaktivieren
|
||||
handCardIds.forEach((id) => {
|
||||
const s = document.getElementById(id);
|
||||
if (s) {
|
||||
s.draggable = false;
|
||||
delete s.dataset.cardSlotId;
|
||||
}
|
||||
}, 26000);
|
||||
});
|
||||
showTurnIndicator(activeName);
|
||||
// Timer für Gegner-Countdown auch anzeigen
|
||||
startTurnTimer(activeName || "Gegner");
|
||||
}
|
||||
}
|
||||
|
||||
function showTurnIndicator() {
|
||||
function showTurnIndicator(activeName) {
|
||||
document.getElementById("turn-indicator")?.remove();
|
||||
const ind = document.createElement("div");
|
||||
ind.id = "turn-indicator";
|
||||
@ -677,34 +669,35 @@
|
||||
text-transform:uppercase;
|
||||
box-shadow:0 4px 20px rgba(0,0,0,0.6);
|
||||
`;
|
||||
ind.textContent = "⏳ Gegner ist am Zug";
|
||||
ind.textContent = activeName
|
||||
? `⏳ ${activeName} ist am Zug`
|
||||
: "⏳ Gegner ist am Zug";
|
||||
document.body.appendChild(ind);
|
||||
}
|
||||
|
||||
/* ── Zug beenden: CD ticken + Karte ziehen + Server informieren ── */
|
||||
/* ── Zug beenden: per Button oder Timer-Ablauf ── */
|
||||
document.getElementById("end-turn-btn")?.addEventListener("click", () => {
|
||||
if (!isMyTurn) return;
|
||||
// Sofort Timer stoppen und Zug abgeben – nicht auf turn_change warten
|
||||
clearInterval(turnTimerInt);
|
||||
stopTurnTimer();
|
||||
tickHandCooldowns();
|
||||
// Eine Karte aus dem Deck nachziehen
|
||||
drawNextCard();
|
||||
setTurnState(false);
|
||||
socket.emit("end_turn", { matchId, slot: mySlot });
|
||||
// Server antwortet mit turn_change → dann wird setTurnState erneut aufgerufen
|
||||
});
|
||||
/* ── Hilfsfunktion: Karte mit Stats in einen Slot rendern ── */
|
||||
function renderCardInSlot(slot, card) {
|
||||
if (!slot || !card) return;
|
||||
const atkVal = card.attack ?? null;
|
||||
const defVal = card.defends ?? null;
|
||||
const cdVal = card.cooldown ?? null;
|
||||
const rngVal = card.range ?? null;
|
||||
const rceVal = card.race ?? null;
|
||||
const cdVal = card.cooldown ?? null;
|
||||
const rngVal = card.range ?? null;
|
||||
const rceVal = card.race ?? null;
|
||||
|
||||
const hasAtk = atkVal != null;
|
||||
const hasDef = defVal != null;
|
||||
const hasCd = cdVal != null;
|
||||
const hasCd = cdVal != null;
|
||||
const hasRng = rngVal != null;
|
||||
const hasRce = rceVal != null;
|
||||
const statsHtml =
|
||||
@ -713,9 +706,9 @@
|
||||
<div class="card-stat-overlay">
|
||||
${hasAtk ? `<span class="cs-atk">${atkVal}</span>` : ""}
|
||||
${hasDef ? `<span class="cs-def">${defVal}</span>` : ""}
|
||||
${hasCd ? `<span class="cs-cd">${cdVal}</span>` : ""}
|
||||
${hasCd ? `<span class="cs-cd">${cdVal}</span>` : ""}
|
||||
${hasRng ? `<span class="cs-range">${SVG_RANGE} ${rngVal}</span>` : ""}
|
||||
${hasRce ? `<span class="cs-race">${SVG_RACE} ${rceVal}</span>` : ""}
|
||||
${hasRce ? `<span class="cs-race">${SVG_RACE} ${rceVal}</span>` : ""}
|
||||
</div>`
|
||||
: "";
|
||||
// note: SVG_RACE is defined as the walking icon above
|
||||
@ -741,15 +734,14 @@
|
||||
const mySlot = urlParams.get("slot") || "<%= mySlot || 'player1' %>";
|
||||
const amIPlayer1 = mySlot === "player1";
|
||||
|
||||
// Board-Klasse setzen damit CSS die richtigen Zonen einfärbt
|
||||
document.querySelector(".board")?.classList.add(
|
||||
amIPlayer1 ? "my-side-left" : "my-side-right"
|
||||
);
|
||||
// Board-Klasse wird erst nach ready_status gesetzt wenn amILeftPlayer bekannt ist
|
||||
|
||||
// Gegner-Name direkt aus URL setzen (kommt aus match_found, immer korrekt)
|
||||
const opponentNameFromUrl = urlParams.get("opponent");
|
||||
if (opponentNameFromUrl) {
|
||||
const oppEl = document.getElementById(amIPlayer1 ? "nameRight" : "nameLeft");
|
||||
const oppEl = document.getElementById(
|
||||
amIPlayer1 ? "nameRight" : "nameLeft",
|
||||
);
|
||||
if (oppEl) oppEl.textContent = decodeURIComponent(opponentNameFromUrl);
|
||||
}
|
||||
|
||||
@ -851,9 +843,17 @@
|
||||
const currentOppName = oppEl?.textContent || "";
|
||||
const urlOppName = urlParams.get("opponent");
|
||||
// Nur überschreiben wenn Server einen echten Namen hat (nicht "Spieler"/"Spieler 1"/"Spieler 2")
|
||||
if (oppEl && oppName && !["Spieler", "Spieler 1", "Spieler 2", "Gegner"].includes(oppName)) {
|
||||
if (
|
||||
oppEl &&
|
||||
oppName &&
|
||||
!["Spieler", "Spieler 1", "Spieler 2", "Gegner"].includes(oppName)
|
||||
) {
|
||||
oppEl.textContent = oppName;
|
||||
} else if (oppEl && urlOppName && (!currentOppName || currentOppName === "Gegner")) {
|
||||
} else if (
|
||||
oppEl &&
|
||||
urlOppName &&
|
||||
(!currentOppName || currentOppName === "Gegner")
|
||||
) {
|
||||
oppEl.textContent = decodeURIComponent(urlOppName);
|
||||
}
|
||||
// Eigenen Namen sichern falls noch nicht gesetzt
|
||||
@ -884,18 +884,36 @@
|
||||
|
||||
/* ── Server: Zugwechsel ──────────────────────────────── */
|
||||
socket.on("turn_change", (data) => {
|
||||
// Sicherheits-Fallback: Overlay entfernen falls noch vorhanden
|
||||
document.getElementById("board-lock-overlay")?.remove();
|
||||
document.getElementById("connecting-overlay")?.remove();
|
||||
|
||||
if (data.boardSync) applyBoardSync(data.boardSync);
|
||||
const nowMyTurn = data.activeSlot === mySlot;
|
||||
console.log("[1v1] turn_change:", data.activeSlot, "| ich:", mySlot, "| meinZug:", nowMyTurn);
|
||||
setTurnState(nowMyTurn);
|
||||
|
||||
// Aktiver Spieler: Slot aus Server-Daten
|
||||
// Linker Spieler = window._leftSlot (gesetzt nach ready_status)
|
||||
// Der linke Spieler fängt immer an.
|
||||
const activeSlot = data.activeSlot;
|
||||
const nowMyTurn = activeSlot === mySlot;
|
||||
|
||||
// Namen des aktiven Spielers für Timer + Indicator
|
||||
const activeNameEl = document.getElementById(
|
||||
activeSlot === "player1" ? "nameLeft" : "nameRight",
|
||||
);
|
||||
const activeName =
|
||||
activeNameEl?.textContent || (nowMyTurn ? "Du" : "Gegner");
|
||||
|
||||
console.log(
|
||||
`[1v1] turn_change: ${activeSlot} | ich: ${mySlot} | meinZug: ${nowMyTurn} | name: ${activeName}`,
|
||||
);
|
||||
setTurnState(nowMyTurn, activeName);
|
||||
});
|
||||
|
||||
socket.on("turn_started", (data) => {
|
||||
setTurnState(data.slot === mySlot);
|
||||
const myT = data.slot === mySlot;
|
||||
const nameEl = document.getElementById(
|
||||
data.slot === "player1" ? "nameLeft" : "nameRight",
|
||||
);
|
||||
setTurnState(myT, nameEl?.textContent || (myT ? "Du" : "Gegner"));
|
||||
});
|
||||
|
||||
/* ── Bereit-System ──────────────────────────────────── */
|
||||
@ -994,9 +1012,38 @@
|
||||
// Festlegen ob ich der linke Spieler bin
|
||||
amILeftPlayer = flip ? mySlot === "player2" : mySlot === "player1";
|
||||
|
||||
// Server startet Zug automatisch wenn beide bereit sind
|
||||
// turn_change kommt vom Server → kein end_turn_init nötig
|
||||
console.log("[1v1] Beide bereit – warte auf turn_change vom Server...");
|
||||
// Board-CSS-Klasse jetzt setzen da amILeftPlayer bekannt ist
|
||||
const board = document.querySelector(".board");
|
||||
if (board) {
|
||||
board.classList.remove("my-side-left", "my-side-right");
|
||||
board.classList.add(
|
||||
amILeftPlayer ? "my-side-left" : "my-side-right",
|
||||
);
|
||||
}
|
||||
|
||||
// Der LINKE Spieler fängt an → amILeftPlayer bestimmt wer turn_change bekommt
|
||||
// Server schickt turn_change mit activeSlot=player1 → wir mappen das auf links
|
||||
// Wenn ich der linke Spieler bin UND player1 bin → ich bin aktiv
|
||||
// Wenn ich der linke Spieler bin aber player2 bin → ich bin auch aktiv (flip)
|
||||
// → setTurnState direkt setzen basierend auf amILeftPlayer
|
||||
const leftSlot = flip ? "player2" : "player1";
|
||||
const iStartLeft = mySlot === leftSlot;
|
||||
console.log(
|
||||
`[1v1] Beide bereit | linker Spieler-Slot: ${leftSlot} | ich (${mySlot}) starte: ${iStartLeft}`,
|
||||
);
|
||||
window._leftSlot = leftSlot;
|
||||
|
||||
// Nur EINER der beiden Clients sendet start_turn_request
|
||||
// Wir nehmen immer player1 als Absender (deterministisch, kein Doppel-Fire)
|
||||
if (mySlot === "player1") {
|
||||
socket.emit("start_turn_request", {
|
||||
matchId,
|
||||
starterSlot: leftSlot,
|
||||
});
|
||||
console.log(
|
||||
`[1v1] start_turn_request gesendet: starterSlot=${leftSlot}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1219,7 +1266,6 @@
|
||||
r.readAsDataURL(file);
|
||||
}
|
||||
|
||||
|
||||
/* ══════════════════════════════════════════════════════
|
||||
DRAG & DROP – Karte aus Hand auf Board legen
|
||||
══════════════════════════════════════════════════════ */
|
||||
@ -1230,7 +1276,10 @@
|
||||
/* Welche Slot-Indizes darf ich bespielen? */
|
||||
function isMyZone(slotIndex) {
|
||||
const idx = Number(slotIndex);
|
||||
return amIPlayer1 ? idx <= 3 : idx >= 9;
|
||||
// amILeftPlayer nach ready_status bekannt; Fallback: amIPlayer1
|
||||
const iAmLeft = amILeftPlayer !== null ? amILeftPlayer : amIPlayer1;
|
||||
// Linker Spieler: Slots 1–3 | Rechter Spieler: Slots 9–11
|
||||
return iAmLeft ? idx <= 3 : idx >= 9;
|
||||
}
|
||||
|
||||
/* Drop-Zones aktivieren / deaktivieren */
|
||||
@ -1250,16 +1299,16 @@
|
||||
function renderCardOnBoard(slotEl, card) {
|
||||
const atkVal = card.attack ?? null;
|
||||
const defVal = card.defends ?? null;
|
||||
const cdVal = card.cooldown ?? null;
|
||||
const rngVal = card.range ?? null;
|
||||
const rceVal = card.race ?? null;
|
||||
const cdVal = card.cooldown ?? null;
|
||||
const rngVal = card.range ?? null;
|
||||
const rceVal = card.race ?? null;
|
||||
const statsHtml = `
|
||||
<div class="card-stat-overlay">
|
||||
${atkVal != null ? `<span class="cs-atk">${atkVal}</span>` : ""}
|
||||
${defVal != null ? `<span class="cs-def">${defVal}</span>` : ""}
|
||||
${cdVal != null ? `<span class="cs-cd">${cdVal}</span>` : ""}
|
||||
${cdVal != null ? `<span class="cs-cd">${cdVal}</span>` : ""}
|
||||
${rngVal != null ? `<span class="cs-range">${SVG_RANGE} ${rngVal}</span>` : ""}
|
||||
${rceVal != null ? `<span class="cs-race">${SVG_RACE} ${rceVal}</span>` : ""}
|
||||
${rceVal != null ? `<span class="cs-race">${SVG_RACE} ${rceVal}</span>` : ""}
|
||||
</div>`;
|
||||
slotEl.classList.add("slot-occupied");
|
||||
slotEl.innerHTML = card.image
|
||||
@ -1280,7 +1329,10 @@
|
||||
|
||||
document.getElementById("handArea").addEventListener("dragstart", (e) => {
|
||||
const slot = e.target.closest("[data-card-slot-id]");
|
||||
if (!slot || !isMyTurn) { e.preventDefault(); return; }
|
||||
if (!slot || !isMyTurn) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
draggedCardSlotId = slot.dataset.cardSlotId;
|
||||
slot.classList.add("dragging");
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
@ -1308,7 +1360,9 @@
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
// Hover-Highlight
|
||||
row.querySelectorAll(".drop-zone-hover").forEach(s => s.classList.remove("drop-zone-hover"));
|
||||
row
|
||||
.querySelectorAll(".drop-zone-hover")
|
||||
.forEach((s) => s.classList.remove("drop-zone-hover"));
|
||||
slot.classList.add("drop-zone-hover");
|
||||
});
|
||||
|
||||
@ -1324,7 +1378,8 @@
|
||||
const idx = Number(slot.dataset.slotIndex);
|
||||
if (!isMyZone(idx) || boardState[slot.id]) return;
|
||||
// Fallback: manche Browser liefern draggedCardSlotId über dataTransfer
|
||||
const sourceId = draggedCardSlotId || e.dataTransfer.getData("text/plain");
|
||||
const sourceId =
|
||||
draggedCardSlotId || e.dataTransfer.getData("text/plain");
|
||||
if (!sourceId) return;
|
||||
|
||||
const cardState = handSlotState[sourceId];
|
||||
@ -1350,7 +1405,9 @@
|
||||
card: cardState.card,
|
||||
});
|
||||
|
||||
console.log(`[1v1] Karte gespielt: ${cardState.card.name} → ${slot.id} (aus ${sourceId})`);
|
||||
console.log(
|
||||
`[1v1] Karte gespielt: ${cardState.card.name} → ${slot.id} (aus ${sourceId})`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1364,18 +1421,22 @@
|
||||
|
||||
const slotEl = document.getElementById(data.boardSlot);
|
||||
if (!slotEl) {
|
||||
console.warn("[1v1] card_played: Slot nicht gefunden:", data.boardSlot);
|
||||
console.warn(
|
||||
"[1v1] card_played: Slot nicht gefunden:",
|
||||
data.boardSlot,
|
||||
);
|
||||
return;
|
||||
}
|
||||
boardState[data.boardSlot] = 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,
|
||||
);
|
||||
});
|
||||
|
||||
/* Wenn Zug wechselt: draggable aktualisieren */
|
||||
const _origSetTurnState = setTurnState;
|
||||
// setTurnState ist bereits definiert, wir patchen es nach
|
||||
|
||||
/* ── Event-Listener ─────────────────────────────────── */
|
||||
document
|
||||
.getElementById("bereit-btn")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user