golfgame/client/card-manager.js

260 lines
9.3 KiB
JavaScript

// CardManager - Manages persistent card DOM elements
// Cards are REAL elements that exist in ONE place and move between locations
class CardManager {
constructor(cardLayer) {
this.cardLayer = cardLayer;
// Map of "playerId-position" -> card element
this.handCards = new Map();
// Special cards
this.deckCard = null;
this.discardCard = null;
this.holdingCard = null;
}
// Initialize cards for a game state
initializeCards(gameState, playerId, getSlotRect, getDeckRect, getDiscardRect) {
this.clear();
// Create cards for each player's hand
for (const player of gameState.players) {
for (let i = 0; i < 6; i++) {
const card = player.cards[i];
const slotKey = `${player.id}-${i}`;
const cardEl = this.createCardElement(card);
// Position at slot (will be updated later if rect not ready)
const rect = getSlotRect(player.id, i);
if (rect && rect.width > 0) {
this.positionCard(cardEl, rect);
} else {
// Start invisible, will be positioned by updateAllPositions
cardEl.style.opacity = '0';
}
this.handCards.set(slotKey, {
element: cardEl,
cardData: card,
playerId: player.id,
position: i
});
this.cardLayer.appendChild(cardEl);
}
}
}
// Create a card DOM element with 3D flip structure
createCardElement(cardData) {
const card = document.createElement('div');
card.className = 'real-card';
card.innerHTML = `
<div class="card-inner">
<div class="card-face card-face-front"></div>
<div class="card-face card-face-back"><span>?</span></div>
</div>
`;
this.updateCardAppearance(card, cardData);
return card;
}
// Update card visual state (face up/down, content)
updateCardAppearance(cardEl, cardData) {
const inner = cardEl.querySelector('.card-inner');
const front = cardEl.querySelector('.card-face-front');
// Reset front classes
front.className = 'card-face card-face-front';
if (!cardData || !cardData.face_up || !cardData.rank) {
// Face down or no data
inner.classList.add('flipped');
front.innerHTML = '';
} else {
// Face up with data
inner.classList.remove('flipped');
if (cardData.rank === '★') {
front.classList.add('joker');
const icon = cardData.suit === 'hearts' ? '🐉' : '👹';
front.innerHTML = `<span class="joker-icon">${icon}</span><span class="joker-label">Joker</span>`;
} else {
const isRed = cardData.suit === 'hearts' || cardData.suit === 'diamonds';
front.classList.add(isRed ? 'red' : 'black');
front.innerHTML = `${cardData.rank}<br>${this.getSuitSymbol(cardData.suit)}`;
}
}
}
getSuitSymbol(suit) {
return { hearts: '♥', diamonds: '♦', clubs: '♣', spades: '♠' }[suit] || '';
}
// Position a card at a rect
positionCard(cardEl, rect, animate = false) {
if (animate) {
cardEl.classList.add('moving');
}
cardEl.style.left = `${rect.left}px`;
cardEl.style.top = `${rect.top}px`;
cardEl.style.width = `${rect.width}px`;
cardEl.style.height = `${rect.height}px`;
if (animate) {
setTimeout(() => cardEl.classList.remove('moving'), 350);
}
}
// Get a hand card by player and position
getHandCard(playerId, position) {
return this.handCards.get(`${playerId}-${position}`);
}
// Update all card positions to match current slot positions
// Returns number of cards successfully positioned
updateAllPositions(getSlotRect) {
let positioned = 0;
for (const [key, cardInfo] of this.handCards) {
const rect = getSlotRect(cardInfo.playerId, cardInfo.position);
if (rect && rect.width > 0) {
this.positionCard(cardInfo.element, rect, false);
// Restore visibility if it was hidden
cardInfo.element.style.opacity = '1';
positioned++;
}
}
return positioned;
}
// Animate a card flip
async flipCard(playerId, position, newCardData, duration = 400) {
const cardInfo = this.getHandCard(playerId, position);
if (!cardInfo) return;
const inner = cardInfo.element.querySelector('.card-inner');
const front = cardInfo.element.querySelector('.card-face-front');
// Set up the front content before flip
front.className = 'card-face card-face-front';
if (newCardData.rank === '★') {
front.classList.add('joker');
const icon = newCardData.suit === 'hearts' ? '🐉' : '👹';
front.innerHTML = `<span class="joker-icon">${icon}</span><span class="joker-label">Joker</span>`;
} else {
const isRed = newCardData.suit === 'hearts' || newCardData.suit === 'diamonds';
front.classList.add(isRed ? 'red' : 'black');
front.innerHTML = `${newCardData.rank}<br>${this.getSuitSymbol(newCardData.suit)}`;
}
// Animate flip
inner.classList.remove('flipped');
await this.delay(duration);
cardInfo.cardData = newCardData;
}
// Animate a swap: hand card goes to discard, new card comes to hand
async animateSwap(playerId, position, oldCardData, newCardData, getSlotRect, getDiscardRect, duration = 300) {
const cardInfo = this.getHandCard(playerId, position);
if (!cardInfo) return;
const slotRect = getSlotRect(playerId, position);
const discardRect = getDiscardRect();
if (!slotRect || !discardRect) return;
if (!oldCardData || !oldCardData.rank) {
// Can't animate without card data - just update appearance
this.updateCardAppearance(cardInfo.element, newCardData);
cardInfo.cardData = newCardData;
return;
}
const cardEl = cardInfo.element;
const inner = cardEl.querySelector('.card-inner');
const front = cardEl.querySelector('.card-face-front');
// Step 1: If face down, flip to reveal the old card
if (!oldCardData.face_up) {
// Set front to show old card
front.className = 'card-face card-face-front';
if (oldCardData.rank === '★') {
front.classList.add('joker');
const icon = oldCardData.suit === 'hearts' ? '🐉' : '👹';
front.innerHTML = `<span class="joker-icon">${icon}</span><span class="joker-label">Joker</span>`;
} else {
const isRed = oldCardData.suit === 'hearts' || oldCardData.suit === 'diamonds';
front.classList.add(isRed ? 'red' : 'black');
front.innerHTML = `${oldCardData.rank}<br>${this.getSuitSymbol(oldCardData.suit)}`;
}
inner.classList.remove('flipped');
await this.delay(400);
}
// Step 2: Move card to discard
cardEl.classList.add('moving');
this.positionCard(cardEl, discardRect);
await this.delay(duration + 50);
cardEl.classList.remove('moving');
// Pause to show the discarded card
await this.delay(250);
// Step 3: Update card to show new card and move back to hand
front.className = 'card-face card-face-front';
if (newCardData.rank === '★') {
front.classList.add('joker');
const icon = newCardData.suit === 'hearts' ? '🐉' : '👹';
front.innerHTML = `<span class="joker-icon">${icon}</span><span class="joker-label">Joker</span>`;
} else {
const isRed = newCardData.suit === 'hearts' || newCardData.suit === 'diamonds';
front.classList.add(isRed ? 'red' : 'black');
front.innerHTML = `${newCardData.rank}<br>${this.getSuitSymbol(newCardData.suit)}`;
}
if (!newCardData.face_up) {
inner.classList.add('flipped');
}
cardEl.classList.add('moving');
this.positionCard(cardEl, slotRect);
await this.delay(duration + 50);
cardEl.classList.remove('moving');
cardInfo.cardData = newCardData;
}
// Set holding state for a card (drawn card highlight)
setHolding(playerId, position, isHolding) {
const cardInfo = this.getHandCard(playerId, position);
if (cardInfo) {
cardInfo.element.classList.toggle('holding', isHolding);
}
}
// Clear all cards
clear() {
for (const [key, cardInfo] of this.handCards) {
cardInfo.element.remove();
}
this.handCards.clear();
if (this.holdingCard) {
this.holdingCard.remove();
this.holdingCard = null;
}
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = CardManager;
}