diff --git a/public/css/1v1.css b/public/css/1v1.css
index 48850a3..025610f 100644
--- a/public/css/1v1.css
+++ b/public/css/1v1.css
@@ -1154,3 +1154,45 @@ body {
border: 1px solid #7de87d;
color: #7de87d;
}
+
+/* ═══════════════════════════════════════════════════════════
+ SPIELER-FARBEN: Blau (links) & Rot (rechts)
+═══════════════════════════════════════════════════════════ */
+
+/* ── Karten auf dem Spielfeld ── */
+.card-slot.slot-owner-left {
+ border-color: rgba(60, 140, 255, 0.9) !important;
+ box-shadow:
+ 0 0 calc(var(--s) * 14) rgba(60, 140, 255, 0.45),
+ inset 0 0 calc(var(--s) * 10) rgba(60, 140, 255, 0.1);
+}
+.card-slot.slot-owner-right {
+ border-color: rgba(220, 60, 60, 0.9) !important;
+ box-shadow:
+ 0 0 calc(var(--s) * 14) rgba(220, 60, 60, 0.45),
+ inset 0 0 calc(var(--s) * 10) rgba(220, 60, 60, 0.1);
+}
+
+/* ── Avatare ── */
+.avatar.av-team-left {
+ border-color: rgba(60, 140, 255, 0.95) !important;
+ box-shadow:
+ 0 0 calc(var(--s) * 28) rgba(60, 140, 255, 0.5),
+ inset 0 0 calc(var(--s) * 20) rgba(0, 0, 0, 0.5) !important;
+}
+.avatar.av-team-left:hover {
+ box-shadow:
+ 0 0 calc(var(--s) * 42) rgba(60, 140, 255, 0.75),
+ inset 0 0 calc(var(--s) * 20) rgba(0, 0, 0, 0.5) !important;
+}
+.avatar.av-team-right {
+ border-color: rgba(220, 60, 60, 0.95) !important;
+ box-shadow:
+ 0 0 calc(var(--s) * 28) rgba(220, 60, 60, 0.5),
+ inset 0 0 calc(var(--s) * 20) rgba(0, 0, 0, 0.5) !important;
+}
+.avatar.av-team-right:hover {
+ box-shadow:
+ 0 0 calc(var(--s) * 42) rgba(220, 60, 60, 0.75),
+ inset 0 0 calc(var(--s) * 20) rgba(0, 0, 0, 0.5) !important;
+}
diff --git a/public/js/buildings/1v1.js b/public/js/buildings/1v1.js
index d6eac88..671edc4 100644
--- a/public/js/buildings/1v1.js
+++ b/public/js/buildings/1v1.js
@@ -289,6 +289,19 @@ document.getElementById('end-turn-btn')?.addEventListener('click', endMyTurn);
*/
const boardState = {};
+/* ── Spieler-Farbe auf Slot oder Avatar anwenden ────────────────── */
+function applyOwnerStyle(el, owner) {
+ if (!el || !owner) return;
+ const isLeft = owner === (window._leftSlot || 'player1');
+ if (isLeft) {
+ el.style.borderColor = 'rgba(60, 140, 255, 0.95)';
+ el.style.boxShadow = '0 0 14px rgba(60, 140, 255, 0.5), inset 0 0 10px rgba(60,140,255,0.08)';
+ } else {
+ el.style.borderColor = 'rgba(220, 60, 60, 0.95)';
+ el.style.boxShadow = '0 0 14px rgba(220, 60, 60, 0.5), inset 0 0 10px rgba(220,60,60,0.08)';
+ }
+}
+
function buildStatsHtml(card) {
const atk = card.attack ?? null;
const def = card.defends ?? null;
@@ -304,9 +317,10 @@ function buildStatsHtml(card) {
`;
}
-function renderCardOnBoard(slotEl, card) {
+function renderCardOnBoard(slotEl, card, owner) {
const statsHtml = buildStatsHtml(card);
slotEl.classList.add('slot-occupied');
+ applyOwnerStyle(slotEl, owner);
slotEl.innerHTML = card.image
? `
${statsHtml}`
: `
\u2694\uFE0F${card.name}
${statsHtml}`;
@@ -326,6 +340,8 @@ function clearBoardSlot(slotId) {
const el = document.getElementById(slotId);
if (!el) return;
el.classList.remove('slot-occupied');
+ el.style.borderColor = '';
+ el.style.boxShadow = '';
el.innerHTML = '\u2736' + slotId.split('-slot-')[1] + '';
}
@@ -382,7 +398,7 @@ function applyBoardSync(cards) {
if (!slotEl || boardState[cd.boardSlot]) return;
const owner = cd.owner ?? (cd.slot ?? 'unknown');
boardState[cd.boardSlot] = { card: cd.card, owner };
- renderCardOnBoard(slotEl, cd.card);
+ renderCardOnBoard(slotEl, cd.card, cd.owner);
});
console.log('[1v1] Board sync:', cards.length, 'Karten');
}
@@ -500,6 +516,18 @@ socket.on('ready_status', data => {
document.getElementById('board-lock-overlay')?.remove();
amILeftPlayer = flip ? mySlot === 'player2' : mySlot === 'player1';
+ // Avatare farbig umranden: links=blau, rechts=rot
+ const _avL = document.getElementById('avLeft');
+ const _avR = document.getElementById('avRight');
+ if (_avL) {
+ _avL.style.borderColor = 'rgba(60, 140, 255, 0.95)';
+ _avL.style.boxShadow = '0 0 28px rgba(60, 140, 255, 0.55), inset 0 0 20px rgba(0,0,0,0.5)';
+ }
+ if (_avR) {
+ _avR.style.borderColor = 'rgba(220, 60, 60, 0.95)';
+ _avR.style.boxShadow = '0 0 28px rgba(220, 60, 60, 0.55), inset 0 0 20px rgba(0,0,0,0.5)';
+ }
+
const board = document.querySelector('.board');
if (board) {
board.classList.remove('my-side-left', 'my-side-right');
@@ -509,6 +537,13 @@ socket.on('ready_status', data => {
const leftSlot = flip ? 'player2' : 'player1';
window._leftSlot = leftSlot;
console.log(`[1v1] linker Slot: ${leftSlot} | ich: ${mySlot}`);
+
+ // Bereits auf dem Board liegende Karten jetzt korrekt einfärben
+ // (boardSync kann vor _leftSlot ankommen)
+ Object.entries(boardState).forEach(([slotId, entry]) => {
+ const el = document.getElementById(slotId);
+ if (el && entry?.owner) applyOwnerStyle(el, entry.owner);
+ });
if (mySlot === 'player1') {
socket.emit('start_turn_request', { matchId, starterSlot: leftSlot });
}
@@ -588,7 +623,7 @@ document.getElementById('handArea').addEventListener('dragend', e => {
/* ── boardState mit owner speichern ─────────────────── */
boardState[slot.id] = { card: cardState.card, owner: mySlot };
- renderCardOnBoard(slot, cardState.card);
+ renderCardOnBoard(slot, cardState.card, mySlot);
slot.classList.remove('drop-zone-active', 'drop-zone-hover');
socket.emit('card_played', {
@@ -607,7 +642,7 @@ socket.on('card_played', data => {
/* ── boardState mit owner des Gegners speichern ─────── */
boardState[data.boardSlot] = { card: data.card, owner: data.slot };
- renderCardOnBoard(slotEl, data.card);
+ renderCardOnBoard(slotEl, data.card, data.slot);
console.log('[1v1] Gegner Karte:', data.card?.name, '->', data.boardSlot);
});
@@ -667,7 +702,7 @@ function applyMoveEvent(ev) {
fromEl.classList.remove('slot-occupied');
fromEl.innerHTML = '\u2736' + ev.from.split('-slot-')[1] + '';
- renderCardOnBoard(toEl, entry.card);
+ renderCardOnBoard(toEl, entry.card, entry.owner);
console.log(`[Combat] Bewegt: ${entry.card.name} ${ev.from} → ${ev.to}`);
}
@@ -722,7 +757,7 @@ function applyFinalBoard(finalBoard) {
const slotEl = document.getElementById(entry.boardSlot);
if (!slotEl) return;
boardState[entry.boardSlot] = { card: entry.card, owner: entry.owner };
- renderCardOnBoard(slotEl, entry.card);
+ renderCardOnBoard(slotEl, entry.card, entry.owner);
});
}
diff --git a/routes/carddeck.route.js b/routes/carddeck.route.js
index b90207c..ded5136 100644
--- a/routes/carddeck.route.js
+++ b/routes/carddeck.route.js
@@ -185,7 +185,7 @@ router.get("/decks/:id/cards", async (req, res) => {
const [cards] = await db.query(
`SELECT
dc.card_id,
- dc.amount,
+ LEAST(dc.amount, COALESCE(uc.amount, 0)) AS amount,
c.name,
c.image,
c.rarity,
@@ -195,10 +195,13 @@ router.get("/decks/:id/cards", async (req, res) => {
c.\`range\`,
c.\`race\`
FROM deck_cards dc
- JOIN cards c ON c.id = dc.card_id
+ JOIN cards c ON c.id = dc.card_id
+ LEFT JOIN user_cards uc ON uc.card_id = dc.card_id
+ AND uc.user_id = ?
WHERE dc.deck_id = ?
+ AND COALESCE(uc.amount, 0) > 0
ORDER BY c.name`,
- [deckId]
+ [userId, deckId]
);
res.json(cards);
} catch (err) {
diff --git a/routes/combine.route.js b/routes/combine.route.js
index 20e9696..956406c 100644
--- a/routes/combine.route.js
+++ b/routes/combine.route.js
@@ -86,7 +86,9 @@ router.post("/cards/combine", requireLogin, async (req, res) => {
}
/* ── 6. Karten immer abziehen (Glücksspiel!) ── */
- if (owned.amount - amount <= 0) {
+ const remainingAfter = owned.amount - amount;
+
+ if (remainingAfter <= 0) {
await db.query(
"DELETE FROM user_cards WHERE user_id = ? AND card_id = ?",
[userId, card_id]
@@ -98,6 +100,30 @@ router.post("/cards/combine", requireLogin, async (req, res) => {
);
}
+ /* ── 6b. Decks synchronisieren ──────────────────────────────────
+ Karten die durch Kombination verbraucht wurden, müssen auch aus
+ allen Decks des Spielers entfernt / reduziert werden.
+ Sonst steht im Deck mehr als der Spieler tatsächlich besitzt.
+ ────────────────────────────────────────────────────────────────── */
+ if (remainingAfter <= 0) {
+ // Keine Exemplare mehr vorhanden → aus allen Decks löschen
+ await db.query(
+ `DELETE dc FROM deck_cards dc
+ JOIN decks d ON d.id = dc.deck_id
+ WHERE d.user_id = ? AND dc.card_id = ?`,
+ [userId, card_id]
+ );
+ } else {
+ // Noch welche vorhanden → Deck-Menge auf verbleibende Menge deckeln
+ await db.query(
+ `UPDATE deck_cards dc
+ JOIN decks d ON d.id = dc.deck_id
+ SET dc.amount = LEAST(dc.amount, ?)
+ WHERE d.user_id = ? AND dc.card_id = ?`,
+ [remainingAfter, userId, card_id]
+ );
+ }
+
/* ── 7. Zufallsroll gegen Chance ── */
const roll = Math.random() * 100;
const success = roll <= chance;
diff --git a/sockets/combat.js b/sockets/combat.js
index 9bd2636..bdb8e17 100644
--- a/sockets/combat.js
+++ b/sockets/combat.js
@@ -1,33 +1,29 @@
/**
* sockets/combat.js – Server-seitige Kampfphasen-Logik für 1v1
*
- * ZWEI PHASEN PRO ZUG:
+ * PRO KARTE: erst bewegen, dann sofort angreifen → dann nächste Karte.
*
- * Phase 1 – BEWEGUNG (nur Karten des aktiven Spielers, vorderste zuerst)
- * Phase 2 – ANGRIFF (nur Karten des aktiven Spielers, Slot 11→1)
+ * Reihenfolge (nur Karten des aktiven Spielers):
+ * Linker Spieler (dir +1): höchste Slot-Zahl zuerst (11→1)
+ * Rechter Spieler (dir -1): niedrigste Slot-Zahl zuerst (1→11)
+ * → Vorderste Karte verarbeitet zuerst, macht Platz für die dahinter.
*/
'use strict';
/**
* @param {Object} boardState – wird in-place verändert
- * @param {string} leftSlot – 'player1' oder 'player2' (wer links steht)
- * @param {string} activeSlot – 'player1' oder 'player2' (wer gerade am Zug ist)
+ * @param {string} leftSlot – wer links steht ('player1'|'player2')
+ * @param {string} activeSlot – wer gerade am Zug ist ('player1'|'player2')
* @returns {Array} – geordnete Event-Liste für den Client
*/
function runCombatPhase(boardState, leftSlot, activeSlot) {
- const events = [];
-
- /* ════════════════════════════════════════════════════════
- PHASE 1: BEWEGUNG
- Nur Karten des aktiven Spielers bewegen sich.
- Vorderste Karte zuerst → Kette kann aufrücken.
- Linker Spieler (dir +1): höchste Slot-Zahl zuerst
- Rechter Spieler (dir -1): niedrigste Slot-Zahl zuerst
- ════════════════════════════════════════════════════════ */
+ const events = [];
+ const isActiveLeft = activeSlot === leftSlot;
+ const dir = isActiveLeft ? 1 : -1;
+ /* ── Karten des aktiven Spielers sammeln ────────────────── */
const myCards = [];
-
for (let slotIndex = 1; slotIndex <= 11; slotIndex++) {
for (const row of ['row1', 'row2']) {
const slotId = `${row}-slot-${slotIndex}`;
@@ -37,19 +33,17 @@ function runCombatPhase(boardState, leftSlot, activeSlot) {
}
}
- const isActiveLeft = activeSlot === leftSlot;
- const dir = isActiveLeft ? 1 : -1;
-
- // Vorderste Karte zuerst
+ /* ── Vorderste Karte zuerst ─────────────────────────────── */
myCards.sort((a, b) => {
const ia = parseInt(a.split('-slot-')[1], 10);
const ib = parseInt(b.split('-slot-')[1], 10);
- return isActiveLeft ? ib - ia : ia - ib; // links: 11→1 | rechts: 1→11
+ return isActiveLeft ? ib - ia : ia - ib;
});
+ /* ── Jede Karte: erst bewegen, dann sofort angreifen ────── */
for (const startSlotId of myCards) {
const entry = boardState[startSlotId];
- if (!entry) continue;
+ if (!entry) continue; // wurde durch vorherigen Angriff bereits entfernt (sollte nicht vorkommen, aber sicher ist sicher)
const { card } = entry;
const row = startSlotId.split('-slot-')[0];
@@ -58,6 +52,7 @@ function runCombatPhase(boardState, leftSlot, activeSlot) {
let currentPos = parseInt(startSlotId.split('-slot-')[1], 10);
let currentSlotId = startSlotId;
+ /* ── 1. BEWEGEN ─────────────────────────────────────── */
for (let step = 0; step < race; step++) {
const nextPos = currentPos + dir;
if (nextPos < 1 || nextPos > 11) break;
@@ -73,51 +68,27 @@ function runCombatPhase(boardState, leftSlot, activeSlot) {
currentSlotId = nextSlotId;
currentPos = nextPos;
}
- }
- /* ════════════════════════════════════════════════════════
- PHASE 2: ANGRIFF
- Nur Karten des aktiven Spielers greifen an.
- Feste Reihenfolge: Slot 11 → 1, row1 vor row2.
- Snapshot nach den Bewegungen.
- ════════════════════════════════════════════════════════ */
-
- const attackOrder = [];
- for (let slotIndex = 11; slotIndex >= 1; slotIndex--) {
- for (const row of ['row1', 'row2']) {
- const slotId = `${row}-slot-${slotIndex}`;
- if (boardState[slotId]?.owner === activeSlot) {
- attackOrder.push(slotId);
- }
- }
- }
-
- for (const slotId of attackOrder) {
- const entry = boardState[slotId];
- if (!entry) continue;
-
- const { card } = entry;
- const row = slotId.split('-slot-')[0];
- const currentPos = parseInt(slotId.split('-slot-')[1], 10);
- const range = card.range ?? 0;
+ /* ── 2. ANGREIFEN (sofort nach der Bewegung) ────────── */
+ const range = card.range ?? 0;
for (let r = 1; r <= range; r++) {
- const targetPos = currentPos + dir * r;
+ const targetPos = currentPos + dir * r;
if (targetPos < 1 || targetPos > 11) break;
const targetSlotId = `${row}-slot-${targetPos}`;
const target = boardState[targetSlotId];
- if (!target) continue; // leeres Feld → weiter scannen
+ if (!target) continue; // leeres Feld → weiter scannen
if (target.owner === activeSlot) continue; // eigene Karte → Range geht hindurch
- // Feindliche Karte → Angriff
+ // Feindliche Karte gefunden → Angriff
const atk = card.attack ?? 0;
target.card = { ...target.card, defends: (target.card.defends ?? 0) - atk };
events.push({
type : 'attack',
- from : slotId,
+ from : currentSlotId,
to : targetSlotId,
damage : atk,
remainingDef: target.card.defends,