From 0fd5ad2ca6673f0d8d5ca54853f402ce033eec05 Mon Sep 17 00:00:00 2001 From: cay Date: Mon, 13 Apr 2026 18:08:43 +0100 Subject: [PATCH] fhj,kzfu --- public/css/1v1.css | 42 ++++++++++++++++++++++ public/js/buildings/1v1.js | 47 ++++++++++++++++++++---- routes/carddeck.route.js | 9 +++-- routes/combine.route.js | 28 ++++++++++++++- sockets/combat.js | 73 ++++++++++++-------------------------- 5 files changed, 138 insertions(+), 61 deletions(-) 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,