From 75884af31538f3dc047fdc87368881a30e052f5c Mon Sep 17 00:00:00 2001 From: cay Date: Tue, 14 Apr 2026 11:52:19 +0100 Subject: [PATCH] sfrthrt --- public/css/1v1.css | 85 ++++++++++++++++++++++++ public/js/buildings/1v1.js | 125 +++++++++++++++++++++++++++++++++++ sockets/1vKI_daily.socket.js | 47 +++++++++++-- sockets/arena.socket.js | 24 +++++++ views/1v1-battlefield.ejs | 6 ++ 5 files changed, 283 insertions(+), 4 deletions(-) diff --git a/public/css/1v1.css b/public/css/1v1.css index 025610f..febcb0c 100644 --- a/public/css/1v1.css +++ b/public/css/1v1.css @@ -1196,3 +1196,88 @@ body { 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; } + +/* ══════════════════════════════════════════════════════ + ABLAGESTAPEL (Discard Zone) +══════════════════════════════════════════════════════ */ +.hand-slot-discard { + position: relative; + width: calc(var(--s) * 105); + height: calc(var(--s) * 160); + border-radius: calc(var(--s) * 9); + border: 2px dashed rgba(180, 80, 60, 0.4); + background: rgba(20, 8, 5, 0.75); + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + cursor: default; + transition: border-color 0.2s, background 0.2s; + box-shadow: 0 calc(var(--s) * 4) calc(var(--s) * 14) rgba(0,0,0,0.35); + margin-right: calc(var(--s) * 8); +} +.hand-slot-discard.drag-over { + border-color: rgba(220, 60, 60, 0.95); + background: rgba(80, 15, 10, 0.7); + box-shadow: 0 0 calc(var(--s) * 20) rgba(220,60,60,0.4); +} + +.discard-stack-wrap { + position: absolute; + inset: calc(var(--s) * 8) calc(var(--s) * 8) calc(var(--s) * 20); +} + +/* Gestapelte Karten-Rückseiten im Ablagestapel */ +.discard-card-back { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; + border-radius: calc(var(--s) * 7); + filter: grayscale(70%) brightness(0.55); + box-shadow: 0 2px 6px rgba(0,0,0,0.6); +} +.discard-card-back:nth-child(1) { transform: translate(-3px, -3px) rotate(-4deg); } +.discard-card-back:nth-child(2) { transform: translate(-1px, -1px) rotate(-1.5deg); } +.discard-card-back:nth-child(3) { transform: translate(0, 0) rotate(1deg); } + +.discard-label { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-family: "Cinzel", serif; + font-size: calc(var(--s) * 9); + color: rgba(200, 100, 80, 0.6); + letter-spacing: calc(var(--s) * 1); + pointer-events: none; + white-space: nowrap; + text-align: center; +} +.discard-stack-wrap:not(:empty) + .discard-label { + display: none; /* Label verstecken wenn Karten drin */ +} + +.discard-count { + position: absolute; + bottom: calc(var(--s) * 3); + left: 0; right: 0; + text-align: center; + font-family: "Cinzel", serif; + font-size: calc(var(--s) * 10); + color: rgba(200, 100, 80, 0.75); + text-shadow: 0 1px 3px rgba(0,0,0,0.8); +} +.discard-count:empty, +.discard-count[data-count="0"] { opacity: 0; } + +/* Board-Karte draggable während eigenem Zug */ +.board-slot-draggable { + cursor: grab !important; +} +.board-slot-draggable:active { + cursor: grabbing !important; +} +.board-slot-draggable.dragging { + opacity: 0.45; +} diff --git a/public/js/buildings/1v1.js b/public/js/buildings/1v1.js index 4355902..116fcaf 100644 --- a/public/js/buildings/1v1.js +++ b/public/js/buildings/1v1.js @@ -233,6 +233,7 @@ function updateTimerUI(secs, activeName) { ═══════════════════════════════════════════════════════════ */ function setTurnState(myTurn, activeName) { isMyTurn = myTurn; + if (myTurn) enableBoardDrag(); else disableBoardDrag(); const btn = document.getElementById('end-turn-btn'); if (!btn) return; handCardIds.forEach(id => renderHandSlot(id)); @@ -652,6 +653,130 @@ document.getElementById('handArea').addEventListener('dragend', e => { }); }); + + +/* ═══════════════════════════════════════════════════════════ + ABLAGESTAPEL – DISCARD SYSTEM + Spieler zieht Karte vom Board → auf Discard-Zone droppen + → Karte weg vom Board, ausgegraut auf Stapel, nicht mehr spielbar +═══════════════════════════════════════════════════════════ */ + +const discardPile = []; // abgeworfene Karten (lokal) + +/* ── Discard-Zone Drag-Events ────────────────────────────── */ +const discardZone = document.getElementById('discard-zone'); + +discardZone.addEventListener('dragover', e => { + if (!isMyTurn) return; + if (!draggedBoardSlotId) return; // nur Board-Karten + e.preventDefault(); + discardZone.classList.add('drag-over'); +}); + +discardZone.addEventListener('dragleave', () => { + discardZone.classList.remove('drag-over'); +}); + +discardZone.addEventListener('drop', e => { + e.preventDefault(); + discardZone.classList.remove('drag-over'); + if (!draggedBoardSlotId || !isMyTurn) return; + discardBoardCard(draggedBoardSlotId); + draggedBoardSlotId = null; +}); + +/* ── Board-Karten draggable machen / entfernen ───────────── */ +let draggedBoardSlotId = null; + +function enableBoardDrag() { + document.querySelectorAll('.card-slot').forEach(slot => { + const idx = Number(slot.dataset.slotIndex); + if (!isMyZone(idx)) return; + if (!boardState[slot.id]) return; + if (boardState[slot.id].owner !== mySlot) return; + + slot.classList.add('board-slot-draggable'); + slot.setAttribute('draggable', 'true'); + + // Event-Listener nur einmal anhängen + if (!slot.dataset.discardListenerAttached) { + slot.dataset.discardListenerAttached = '1'; + slot.addEventListener('dragstart', e => { + if (!isMyTurn) { e.preventDefault(); return; } + draggedBoardSlotId = slot.id; + slot.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/board', slot.id); + }); + slot.addEventListener('dragend', () => { + slot.classList.remove('dragging'); + draggedBoardSlotId = null; + discardZone.classList.remove('drag-over'); + }); + } + }); +} + +function disableBoardDrag() { + document.querySelectorAll('.board-slot-draggable').forEach(slot => { + slot.classList.remove('board-slot-draggable'); + slot.setAttribute('draggable', 'false'); + }); +} + +/* ── Karte abwerfen ──────────────────────────────────────── */ +function discardBoardCard(slotId) { + const entry = boardState[slotId]; + if (!entry || entry.owner !== mySlot) return; + + const card = entry.card; + discardPile.push(card); + delete boardState[slotId]; + + // Slot leeren + const slotEl = document.getElementById(slotId); + if (slotEl) { + slotEl.innerHTML = ''; + slotEl.classList.remove('board-slot-draggable', 'dragging'); + slotEl.setAttribute('draggable', 'false'); + delete slotEl.dataset.discardListenerAttached; + } + + updateDiscardZone(); + + socket.emit('discard_card', { matchId, slotId, slot: mySlot, cardId: card.id }); + console.log(`[Discard] Karte abgeworfen: ${card.name} von ${slotId}`); +} + +/* ── Discard-Zone visuell updaten ────────────────────────── */ +function updateDiscardZone() { + const stack = document.getElementById('discard-stack'); + const count = document.getElementById('discard-count'); + if (!stack) return; + + const shown = Math.min(discardPile.length, 3); + stack.innerHTML = Array.from({ length: shown }).map(() => + `Abgeworfen` + ).join(''); + + if (count) { + count.textContent = discardPile.length > 0 ? discardPile.length : ''; + count.dataset.count = discardPile.length; + } +} + +/* ── Gegner wirft Karte ab → Slot leeren ─────────────────── */ +socket.on('card_discarded', data => { + const { slotId } = data; + if (!slotId) return; + const slotEl = document.getElementById(slotId); + if (slotEl) slotEl.innerHTML = ''; + delete boardState[slotId]; + console.log(`[Discard] Gegner hat Karte abgeworfen von ${slotId}`); +}); + +/* ── Board-Drag bei Zugwechsel aktivieren/deaktivieren ────── */ +// Wird von setTurnState aufgerufen (siehe unten) socket.on('card_played', data => { if (data.slot === mySlot) return; if (boardState[data.boardSlot]) return; diff --git a/sockets/1vKI_daily.socket.js b/sockets/1vKI_daily.socket.js index b909986..dfccaea 100644 --- a/sockets/1vKI_daily.socket.js +++ b/sockets/1vKI_daily.socket.js @@ -153,14 +153,23 @@ async function askHaiku(boardState, aiHand, availableSlots, aiSlot, leftSlot) { defends: c.defends ?? 0, race: c.race ?? 0, range: c.range ?? 1, })); + // KI-eigene Karten auf dem Board (könnten abgeworfen werden) + const myBoardCards = Object.entries(boardState) + .filter(([, e]) => e.owner === aiSlot) + .map(([slot, e]) => ({ slot, name: e.card.name, atk: e.card.attack ?? 0 })); + const prompt = `Kartenspiel. Du bist die KI (${isLeft ? 'linke Seite, Slots 1-3, bewegst dich nach rechts' : 'rechte Seite, Slots 9-11, bewegst dich nach links'}). Board: ${JSON.stringify(boardSummary)} +Deine eigenen Karten auf dem Board: ${JSON.stringify(myBoardCards)} Deine Hand: ${JSON.stringify(handSummary)} Freie Slots: ${JSON.stringify(availableSlots)} -Wähle eine Karte und einen freien Slot. Bevorzuge Karten mit hohem Angriff oder hoher Bewegung (race). -Antworte NUR mit validem JSON: {"card_index":0,"slot":"row1-slot-9"} -Wenn keine Karte oder kein Slot: {"skip":true}`; +Optionen: +1. Karte ausspielen: {"card_index":0,"slot":"row1-slot-9"} +2. Eigene Boardkarte abwerfen (wenn sie schlecht positioniert ist): {"discard_slot":"row1-slot-9"} +3. Nichts tun: {"skip":true} + +Antworte NUR mit validem JSON.`; try { const res = await fetch('https://api.anthropic.com/v1/messages', { @@ -213,7 +222,20 @@ async function playAiTurn(io, matchId) { aiRoom.boardState, readyCards, freeSlots, aiRoom.aiSlot, aiRoom.leftSlot ); - if (decision && !decision.skip && decision.card_index != null && decision.slot) { + // KI wirft eigene Boardkarte ab + if (decision && decision.discard_slot) { + const dsSlot = decision.discard_slot; + if (aiRoom.boardState[dsSlot]?.owner === aiRoom.aiSlot) { + delete aiRoom.boardState[dsSlot]; + const ioRoom = io._arenaRooms?.get(matchId); + if (ioRoom?.boardState) delete ioRoom.boardState[dsSlot]; + io.to(aiRoom.playerSocketId).emit('card_discarded', { slotId: dsSlot }); + console.log(`[HT] KI wirft ab: ${dsSlot}`); + await sleep(400); + } + } + + if (decision && !decision.skip && !decision.discard_slot && decision.card_index != null && decision.slot) { const card = readyCards[decision.card_index]; const slotId = decision.slot; @@ -514,6 +536,23 @@ function registerHimmelstorHandlers(io, socket) { console.log(`[HT] arena_join KI-Match: ${matchId}`); }); + + /* ── Spieler wirft Karte ab (KI-Match) ───────────────────── */ + socket.on('discard_card', (data) => { + const { matchId, slotId, slot } = data; + if (!matchId || !htAiMatchIds.has(matchId)) return; + + const aiRoom = htAiRooms.get(matchId); + if (!aiRoom || aiRoom.gameOver) return; + if (slot !== aiRoom.playerSlot) return; // nur eigene Karten + + if (aiRoom.boardState) delete aiRoom.boardState[slotId]; + const ioRoom = io._arenaRooms?.get(matchId); + if (ioRoom?.boardState) delete ioRoom.boardState[slotId]; + + console.log(`[HT] Spieler wirft ab: ${slotId} | Match ${matchId}`); + }); + /* ── end_turn für KI-Matches ────────────────────────────── */ socket.on('end_turn', async (data) => { const { matchId, slot } = data; diff --git a/sockets/arena.socket.js b/sockets/arena.socket.js index 76d214b..1692028 100644 --- a/sockets/arena.socket.js +++ b/sockets/arena.socket.js @@ -817,6 +817,30 @@ function registerArenaHandlers(io, socket) { }); /* ── Zug beenden → Kampfphase → Zugwechsel ── */ + + /* ── Karte vom Board abwerfen ────────────────────────────── */ + socket.on('discard_card', (data) => { + const { matchId, slotId, slot } = data; + if (!matchId || !slotId) return; + if (htAiMatchIds.has(matchId)) return; // KI-Matches: handled by himmelstor.socket + + const room = io._arenaRooms?.get(matchId); + if (!room || room.gameOver) return; + if (room.sockets[slot] !== socket.id) return; // nur eigene Karten + + // Karte aus boardState entfernen + if (room.boardState) delete room.boardState[slotId]; + room.boardCards = room.boardCards?.filter(c => c.boardSlot !== slotId) ?? []; + + // Gegner informieren + const oppSlot = slot === 'player1' ? 'player2' : 'player1'; + const oppSocket = room.sockets[oppSlot]; + if (oppSocket) { + io.to(oppSocket).emit('card_discarded', { slotId }); + } + console.log(`[Arena] Karte abgeworfen: ${slotId} von ${slot} | Match ${matchId}`); + }); + socket.on("end_turn", async (data) => { const { matchId, slot } = data; if (!matchId || !slot) return; diff --git a/views/1v1-battlefield.ejs b/views/1v1-battlefield.ejs index e8ea391..67bcf01 100644 --- a/views/1v1-battlefield.ejs +++ b/views/1v1-battlefield.ejs @@ -89,6 +89,12 @@
+ +
+
+
Ablage
+
0
+