sfrthrt
This commit is contained in:
parent
33460b025d
commit
75884af315
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user