This commit is contained in:
cay 2026-04-13 18:08:43 +01:00
parent 3b173ec9de
commit 0fd5ad2ca6
5 changed files with 138 additions and 61 deletions

View File

@ -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;
}

View File

@ -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) {
</div>`;
}
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
? `<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;">\u2694\uFE0F</span><span style="font-size:9px;color:#f0d9a6;text-align:center;">${card.name}</span></div>${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 = '<span class="slot-icon">\u2736</span><span class="slot-num">' + slotId.split('-slot-')[1] + '</span>';
}
@ -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 = '<span class="slot-icon">\u2736</span><span class="slot-num">' + ev.from.split('-slot-')[1] + '</span>';
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);
});
}

View File

@ -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,
@ -196,9 +196,12 @@ router.get("/decks/:id/cards", async (req, res) => {
c.\`race\`
FROM deck_cards dc
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) {

View File

@ -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;

View File

@ -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 111)
* Reihenfolge (nur Karten des aktiven Spielers):
* Linker Spieler (dir +1): höchste Slot-Zahl zuerst (111)
* Rechter Spieler (dir -1): niedrigste Slot-Zahl zuerst (111)
* 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 = [];
const isActiveLeft = activeSlot === leftSlot;
const dir = isActiveLeft ? 1 : -1;
/*
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
*/
/* ── 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,32 +68,8 @@ 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);
/* ── 2. ANGREIFEN (sofort nach der Bewegung) ────────── */
const range = card.range ?? 0;
for (let r = 1; r <= range; r++) {
@ -111,13 +82,13 @@ function runCombatPhase(boardState, leftSlot, activeSlot) {
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,