This commit is contained in:
cay 2026-04-14 11:52:19 +01:00
parent 33460b025d
commit 75884af315
5 changed files with 283 additions and 4 deletions

View File

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

View File

@ -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(() =>
`<img class="discard-card-back" src="/images/items/rueckseite.png" alt="Abgeworfen" draggable="false">`
).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;

View File

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

View File

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

View File

@ -89,6 +89,12 @@
</div>
<div class="bottom-bar">
<!-- Ablagestapel: links neben dem Deck -->
<div id="discard-zone" class="hand-slot-discard" title="Karte abwerfen">
<div class="discard-stack-wrap" id="discard-stack"></div>
<div class="discard-label">Ablage</div>
<div class="discard-count" id="discard-count">0</div>
</div>
<div class="hand-area" id="handArea"></div>
<div class="action-hud">
<div class="action-row">