This commit is contained in:
cay 2026-04-12 10:32:09 +01:00
parent c9dbd6c842
commit 13e9e1e0fb
3 changed files with 222 additions and 7 deletions

View File

@ -885,3 +885,48 @@ body {
opacity: 0.25;
pointer-events: none;
}
/*
DRAG & DROP
*/
/* Hand-Karte wird gerade gezogen */
.hand-slot.dragging {
opacity: 0.35;
transform: scale(0.95);
border-color: rgba(240, 190, 60, 0.4) !important;
}
/* Board-Slot ist eine aktive Drop-Zone (freier Slot in meiner Zone) */
.card-slot.drop-zone-active {
border: 2px dashed rgba(240, 190, 60, 0.9) !important;
background: rgba(180, 130, 20, 0.22) !important;
animation: pulse-zone 1.2s ease-in-out infinite;
}
/* Hover: Karte schwebt über einem gültigen Slot */
.card-slot.drop-zone-hover {
border: 2px solid rgba(240, 210, 80, 1) !important;
background: rgba(200, 150, 30, 0.38) !important;
box-shadow:
0 0 0 3px rgba(240, 200, 60, 0.4),
0 0 28px rgba(240, 190, 60, 0.55) !important;
transform: scale(1.06);
animation: none !important;
}
@keyframes pulse-zone {
0%, 100% { box-shadow: 0 0 8px rgba(240, 190, 60, 0.25); }
50% { box-shadow: 0 0 22px rgba(240, 190, 60, 0.55); }
}
/* Hand-Slot nicht draggable (kein Cursor-Feedback) */
.hand-slot:not([draggable="true"]) {
cursor: default;
}
.hand-slot[draggable="true"] {
cursor: grab;
}
.hand-slot[draggable="true"]:active {
cursor: grabbing;
}

View File

@ -1,5 +1,5 @@
/* ============================================================
sockets/arena.js
sockets/arena.js
1v1 Matchmaking + 2v2 Team-Lobby + 4v4 Team-Lobby
inkl. Kick-Funktion für Team-Leader wurde getestet
============================================================ */
@ -21,7 +21,7 @@ function generateId() {
}
/*
HELPER: Generisch für 2v2 UND 4v4 ohne 1v1
HELPER: Generisch für 2v2 UND 4v4
*/
function getTeamMap(mode) {
return mode === "4v4" ? teams4v4 : teams2v2;
@ -415,9 +415,7 @@ 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);
@ -472,6 +470,14 @@ function registerArenaHandlers(io, socket) {
console.log(`[1v1] Zug: ${slot}${nextSlot} | Match ${matchId}`);
});
/* ── Karte gespielt → an Gegner weiterleiten ── */
socket.on("card_played", (data) => {
const { matchId } = data;
if (!matchId) return;
socket.to("arena_" + matchId).emit("card_played", data);
console.log(`[1v1] card_played: ${data.card?.name}${data.boardSlot} | Match ${matchId}`);
});
socket.on("end_turn_init", (data) => {
const { matchId, starterSlot } = data;
if (!matchId || !starterSlot) return;

View File

@ -351,11 +351,14 @@
<script src="/socket.io/socket.io.js"></script>
<script>
/* ── Spielfeld aufbauen ─────────────────────────────── */
["row1", "row2"].forEach((id) => {
const row = document.getElementById(id);
["row1", "row2"].forEach((rowId) => {
const row = document.getElementById(rowId);
for (let i = 1; i <= 11; i++) {
const s = document.createElement("div");
s.className = "card-slot";
s.dataset.row = rowId;
s.dataset.slotIndex = i;
s.id = `${rowId}-slot-${i}`;
s.innerHTML =
'<span class="slot-icon">✦</span><span class="slot-num">' +
i +
@ -465,6 +468,15 @@
</div>${statsHtml}${readyBadge}`;
slot.classList.toggle("hand-slot-ready", isReady);
// Drag & Drop: nur wenn Karte bereit UND mein Zug
if (isReady && isMyTurn) {
slot.draggable = true;
slot.dataset.cardSlotId = id;
} else {
slot.draggable = false;
delete slot.dataset.cardSlotId;
}
}
/* ── Karte in Hand-Slot legen ──────────────────────── */
@ -614,6 +626,8 @@
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
@ -625,6 +639,8 @@
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);
@ -1184,6 +1200,154 @@
r.readAsDataURL(file);
}
/* ══════════════════════════════════════════════════════
DRAG & DROP Karte aus Hand auf Board legen
══════════════════════════════════════════════════════ */
// Board-State: welche Karten liegen auf welchem Slot
const boardState = {}; // key: "row1-slot-3", value: card object
/* Welche Slot-Indizes darf ich bespielen? */
function isMyZone(slotIndex) {
const idx = Number(slotIndex);
return amIPlayer1 ? idx <= 3 : idx >= 9;
}
/* Drop-Zones aktivieren / deaktivieren */
function setDropZones(active) {
document.querySelectorAll(".card-slot").forEach((slot) => {
const idx = Number(slot.dataset.slotIndex);
if (!isMyZone(idx)) return;
if (active && !boardState[slot.id]) {
slot.classList.add("drop-zone-active");
} else {
slot.classList.remove("drop-zone-active", "drop-zone-hover");
}
});
}
/* Karte auf Board rendern */
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 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>` : ""}
${rngVal != null ? `<span class="cs-range">${SVG_RANGE}&thinsp;${rngVal}</span>` : ""}
${rceVal != null ? `<span class="cs-race">${SVG_RACE}&thinsp;${rceVal}</span>` : ""}
</div>`;
slotEl.classList.add("slot-occupied");
slotEl.innerHTML = card.image
? `<img src="/images/cards/${card.image}"
onerror="this.src='/images/items/rueckseite.png'"
title="${card.name}"
style="width:100%;height:100%;object-fit:cover;border-radius:7px;display:block;">
${statsHtml}`
: `<div style="display:flex;flex-direction:column;align-items:center;
justify-content:center;height:100%;gap:4px;font-family:Cinzel,serif;padding:4px;">
<span style="font-size:18px;">⚔️</span>
<span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span>
</div>${statsHtml}`;
}
/* ── Drag-Events auf Hand-Slots (delegiert) ── */
let draggedCardSlotId = null;
document.getElementById("handArea").addEventListener("dragstart", (e) => {
const slot = e.target.closest("[data-card-slot-id]");
if (!slot || !isMyTurn) { e.preventDefault(); return; }
draggedCardSlotId = slot.dataset.cardSlotId;
slot.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", draggedCardSlotId);
// Drop-Zones einblenden
setTimeout(() => setDropZones(true), 0);
});
document.getElementById("handArea").addEventListener("dragend", (e) => {
const slot = e.target.closest("[data-card-slot-id]");
if (slot) slot.classList.remove("dragging");
setDropZones(false);
draggedCardSlotId = null;
});
/* ── Drop-Events auf Board-Slots (delegiert) ── */
["row1", "row2"].forEach((rowId) => {
const row = document.getElementById(rowId);
row.addEventListener("dragover", (e) => {
const slot = e.target.closest(".card-slot");
if (!slot) return;
const idx = Number(slot.dataset.slotIndex);
if (!isMyZone(idx) || boardState[slot.id]) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
// Hover-Highlight
row.querySelectorAll(".drop-zone-hover").forEach(s => s.classList.remove("drop-zone-hover"));
slot.classList.add("drop-zone-hover");
});
row.addEventListener("dragleave", (e) => {
const slot = e.target.closest(".card-slot");
if (slot) slot.classList.remove("drop-zone-hover");
});
row.addEventListener("drop", (e) => {
e.preventDefault();
const slot = e.target.closest(".card-slot");
if (!slot) return;
const idx = Number(slot.dataset.slotIndex);
if (!isMyZone(idx) || boardState[slot.id]) return;
if (!draggedCardSlotId) return;
const cardState = handSlotState[draggedCardSlotId];
if (!cardState || cardState.currentCd > 0) return;
// Karte vom Hand-Slot entfernen
handSlotState[draggedCardSlotId] = null;
renderHandSlot(draggedCardSlotId);
// Karte auf Board-Slot legen
boardState[slot.id] = cardState.card;
renderCardOnBoard(slot, cardState.card);
slot.classList.remove("drop-zone-active", "drop-zone-hover");
// Nächste Karte nachladen
drawNextCard();
// Gegner & Server informieren
socket.emit("card_played", {
matchId,
slot: mySlot,
boardSlot: slot.id,
row: slot.dataset.row,
slotIndex: idx,
card: cardState.card,
});
console.log(`[1v1] Karte gespielt: ${cardState.card.name} → ${slot.id}`);
});
});
/* ── Gegner hat Karte gespielt → auf Board anzeigen ── */
socket.on("card_played", (data) => {
if (data.slot === mySlot) return; // eigene Aktion, bereits lokal gesetzt
const slotEl = document.getElementById(data.boardSlot);
if (!slotEl) return;
boardState[data.boardSlot] = data.card;
renderCardOnBoard(slotEl, data.card);
});
/* Wenn Zug wechselt: draggable aktualisieren */
const _origSetTurnState = setTurnState;
// setTurnState ist bereits definiert, wir patchen es nach
/* ── Event-Listener ─────────────────────────────────── */
document
.getElementById("bereit-btn")