Additional animation work and AI strategy enhancements and logging for performance analytics.
This commit is contained in:
parent
f80bab3b4b
commit
0f44464c4f
472
client/app.js
472
client/app.js
@ -1,9 +1,5 @@
|
|||||||
// Golf Card Game - Client Application
|
// Golf Card Game - Client Application
|
||||||
|
|
||||||
// Feature flag for new persistent card system
|
|
||||||
// Disabled - using improved legacy system instead
|
|
||||||
const USE_NEW_CARD_SYSTEM = false;
|
|
||||||
|
|
||||||
class GolfGame {
|
class GolfGame {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
@ -19,37 +15,24 @@ class GolfGame {
|
|||||||
this.soundEnabled = true;
|
this.soundEnabled = true;
|
||||||
this.audioCtx = null;
|
this.audioCtx = null;
|
||||||
|
|
||||||
// Swap animation state (legacy)
|
// Swap animation state
|
||||||
this.swapAnimationInProgress = false;
|
this.swapAnimationInProgress = false;
|
||||||
this.swapAnimationCardEl = null;
|
this.swapAnimationCardEl = null;
|
||||||
this.swapAnimationFront = null;
|
this.swapAnimationFront = null;
|
||||||
this.pendingGameState = null;
|
this.pendingGameState = null;
|
||||||
|
|
||||||
// New card system state
|
|
||||||
this.previousState = null;
|
|
||||||
this.isAnimating = false;
|
|
||||||
|
|
||||||
// Track cards we've locally flipped (for immediate feedback during selection)
|
// Track cards we've locally flipped (for immediate feedback during selection)
|
||||||
this.locallyFlippedCards = new Set();
|
this.locallyFlippedCards = new Set();
|
||||||
|
|
||||||
// Animation lock - prevent overlapping animations on same elements
|
// Animation lock - prevent overlapping animations on same elements
|
||||||
this.animatingPositions = new Set();
|
this.animatingPositions = new Set();
|
||||||
|
|
||||||
|
// Track round winners for visual highlight
|
||||||
|
this.roundWinnerNames = new Set();
|
||||||
|
|
||||||
this.initElements();
|
this.initElements();
|
||||||
this.initAudio();
|
this.initAudio();
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
|
|
||||||
// Initialize new card system
|
|
||||||
if (USE_NEW_CARD_SYSTEM) {
|
|
||||||
this.initNewCardSystem();
|
|
||||||
|
|
||||||
// Update card positions on resize
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
if (this.cardManager && this.gameState) {
|
|
||||||
this.cardManager.updateAllPositions((pid, pos) => this.getSlotRect(pid, pos));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initAudio() {
|
initAudio() {
|
||||||
@ -128,167 +111,10 @@ class GolfGame {
|
|||||||
this.playSound('click');
|
this.playSound('click');
|
||||||
}
|
}
|
||||||
|
|
||||||
initNewCardSystem() {
|
|
||||||
const cardLayer = document.getElementById('card-layer');
|
|
||||||
this.cardManager = new CardManager(cardLayer);
|
|
||||||
this.stateDiffer = new StateDiffer();
|
|
||||||
this.animationQueue = new AnimationQueue(
|
|
||||||
this.cardManager,
|
|
||||||
(playerId, position) => this.getSlotRect(playerId, position),
|
|
||||||
(location) => this.getLocationRectNew(location),
|
|
||||||
(type) => this.playSound(type)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the bounding rect of a card slot
|
|
||||||
getSlotRect(playerId, position) {
|
|
||||||
// Try to find by data attribute first (new system)
|
|
||||||
const slotByData = document.querySelector(`.card-slot[data-player="${playerId}"][data-position="${position}"]`);
|
|
||||||
if (slotByData) {
|
|
||||||
const rect = slotByData.getBoundingClientRect();
|
|
||||||
if (rect.width > 0) return rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Check if it's the local player
|
|
||||||
if (playerId === this.playerId) {
|
|
||||||
const slots = this.playerCards.querySelectorAll('.card, .card-slot');
|
|
||||||
if (slots[position]) {
|
|
||||||
return slots[position].getBoundingClientRect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get rect for deck/discard/holding locations
|
|
||||||
getLocationRectNew(location) {
|
|
||||||
switch (location) {
|
|
||||||
case 'deck':
|
|
||||||
return this.deck.getBoundingClientRect();
|
|
||||||
case 'discard':
|
|
||||||
return this.discard.getBoundingClientRect();
|
|
||||||
case 'holding': {
|
|
||||||
const rect = this.discard.getBoundingClientRect();
|
|
||||||
return {
|
|
||||||
left: rect.left,
|
|
||||||
top: rect.top,
|
|
||||||
width: rect.width,
|
|
||||||
height: rect.height
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize persistent cards for a new game/round
|
|
||||||
initializePersistentCards() {
|
|
||||||
if (!this.cardManager || !this.gameState) return;
|
|
||||||
|
|
||||||
this.cardManager.initializeCards(
|
|
||||||
this.gameState,
|
|
||||||
this.playerId,
|
|
||||||
(pid, pos) => this.getSlotRect(pid, pos),
|
|
||||||
() => this.deck.getBoundingClientRect(),
|
|
||||||
() => this.discard.getBoundingClientRect()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Retry positioning a few times to handle layout delays
|
|
||||||
let retries = 0;
|
|
||||||
const tryPosition = () => {
|
|
||||||
const positioned = this.cardManager.updateAllPositions((pid, pos) => this.getSlotRect(pid, pos));
|
|
||||||
retries++;
|
|
||||||
if (retries < 5) {
|
|
||||||
requestAnimationFrame(tryPosition);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
requestAnimationFrame(tryPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate persistent cards based on detected movements
|
|
||||||
async animatePersistentCards(movements, newState) {
|
|
||||||
if (!this.cardManager) return;
|
|
||||||
|
|
||||||
for (const movement of movements) {
|
|
||||||
switch (movement.type) {
|
|
||||||
case 'flip':
|
|
||||||
this.playSound('flip');
|
|
||||||
await this.cardManager.flipCard(
|
|
||||||
movement.playerId,
|
|
||||||
movement.position,
|
|
||||||
movement.card
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'swap':
|
|
||||||
this.playSound('flip');
|
|
||||||
await this.cardManager.animateSwap(
|
|
||||||
movement.playerId,
|
|
||||||
movement.position,
|
|
||||||
movement.oldCard,
|
|
||||||
movement.newCard,
|
|
||||||
(pid, pos) => this.getSlotRect(pid, pos),
|
|
||||||
() => this.discard.getBoundingClientRect()
|
|
||||||
);
|
|
||||||
this.playSound('card');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'draw-deck':
|
|
||||||
case 'draw-discard':
|
|
||||||
this.playSound('card');
|
|
||||||
await this.delay(200);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small pause between animations
|
|
||||||
await this.delay(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delay(ms) {
|
delay(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update persistent card positions and visual states
|
|
||||||
updatePersistentCards() {
|
|
||||||
if (!this.cardManager || !this.gameState) return;
|
|
||||||
|
|
||||||
// If cards haven't been created yet, initialize them
|
|
||||||
if (this.cardManager.handCards.size === 0) {
|
|
||||||
// Need to wait for DOM to have slots - already handled in game_started
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update positions (in case window resized)
|
|
||||||
this.cardManager.updateAllPositions((pid, pos) => this.getSlotRect(pid, pos));
|
|
||||||
|
|
||||||
// Update card visual states (clickable, selected)
|
|
||||||
const myData = this.getMyPlayerData();
|
|
||||||
if (myData) {
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
const cardInfo = this.cardManager.getHandCard(this.playerId, i);
|
|
||||||
if (cardInfo) {
|
|
||||||
const card = myData.cards[i];
|
|
||||||
const isClickable = (
|
|
||||||
(this.gameState.waiting_for_initial_flip && !card.face_up) ||
|
|
||||||
(this.drawnCard) ||
|
|
||||||
(this.waitingForFlip && !card.face_up)
|
|
||||||
);
|
|
||||||
const isSelected = this.selectedCards.includes(i);
|
|
||||||
|
|
||||||
cardInfo.element.classList.toggle('clickable', isClickable);
|
|
||||||
cardInfo.element.classList.toggle('selected', isSelected);
|
|
||||||
|
|
||||||
// Make card clickable
|
|
||||||
if (!cardInfo.element._hasClickHandler) {
|
|
||||||
const pos = i;
|
|
||||||
cardInfo.element.addEventListener('click', () => this.handleCardClick(pos));
|
|
||||||
cardInfo.element._hasClickHandler = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initElements() {
|
initElements() {
|
||||||
// Screens
|
// Screens
|
||||||
this.lobbyScreen = document.getElementById('lobby-screen');
|
this.lobbyScreen = document.getElementById('lobby-screen');
|
||||||
@ -345,6 +171,7 @@ class GolfGame {
|
|||||||
this.discardContent = document.getElementById('discard-content');
|
this.discardContent = document.getElementById('discard-content');
|
||||||
this.discardBtn = document.getElementById('discard-btn');
|
this.discardBtn = document.getElementById('discard-btn');
|
||||||
this.playerCards = document.getElementById('player-cards');
|
this.playerCards = document.getElementById('player-cards');
|
||||||
|
this.playerArea = this.playerCards.closest('.player-area');
|
||||||
this.swapAnimation = document.getElementById('swap-animation');
|
this.swapAnimation = document.getElementById('swap-animation');
|
||||||
this.swapCardFromHand = document.getElementById('swap-card-from-hand');
|
this.swapCardFromHand = document.getElementById('swap-card-from-hand');
|
||||||
this.scoreboard = document.getElementById('scoreboard');
|
this.scoreboard = document.getElementById('scoreboard');
|
||||||
@ -479,6 +306,11 @@ class GolfGame {
|
|||||||
|
|
||||||
case 'game_started':
|
case 'game_started':
|
||||||
case 'round_started':
|
case 'round_started':
|
||||||
|
// Clear any countdown from previous hole
|
||||||
|
this.clearNextHoleCountdown();
|
||||||
|
this.nextRoundBtn.classList.remove('waiting');
|
||||||
|
// Clear round winner highlights
|
||||||
|
this.roundWinnerNames = new Set();
|
||||||
this.gameState = data.game_state;
|
this.gameState = data.game_state;
|
||||||
// Deep copy for previousState to avoid reference issues
|
// Deep copy for previousState to avoid reference issues
|
||||||
this.previousState = JSON.parse(JSON.stringify(data.game_state));
|
this.previousState = JSON.parse(JSON.stringify(data.game_state));
|
||||||
@ -487,48 +319,11 @@ class GolfGame {
|
|||||||
this.animatingPositions = new Set();
|
this.animatingPositions = new Set();
|
||||||
this.playSound('shuffle');
|
this.playSound('shuffle');
|
||||||
this.showGameScreen();
|
this.showGameScreen();
|
||||||
if (USE_NEW_CARD_SYSTEM && this.cardManager) {
|
|
||||||
this.cardManager.clear(); // Clear any leftover cards
|
|
||||||
}
|
|
||||||
this.renderGame();
|
this.renderGame();
|
||||||
// Initialize persistent cards after DOM is ready
|
|
||||||
if (USE_NEW_CARD_SYSTEM && this.cardManager) {
|
|
||||||
setTimeout(() => this.initializePersistentCards(), 50);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'game_state':
|
case 'game_state':
|
||||||
if (USE_NEW_CARD_SYSTEM) {
|
// State updates are instant, animations are fire-and-forget
|
||||||
// New card system: animate persistent cards directly
|
|
||||||
if (this.isAnimating) {
|
|
||||||
this.pendingGameState = data.game_state;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const movements = this.stateDiffer.diff(this.previousState, data.game_state);
|
|
||||||
|
|
||||||
if (movements.length > 0) {
|
|
||||||
this.isAnimating = true;
|
|
||||||
this.animatePersistentCards(movements, data.game_state).then(() => {
|
|
||||||
this.isAnimating = false;
|
|
||||||
this.gameState = data.game_state;
|
|
||||||
this.previousState = JSON.parse(JSON.stringify(data.game_state));
|
|
||||||
this.renderGame();
|
|
||||||
|
|
||||||
if (this.pendingGameState) {
|
|
||||||
const pending = this.pendingGameState;
|
|
||||||
this.pendingGameState = null;
|
|
||||||
this.handleMessage({ type: 'game_state', game_state: pending });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.gameState = data.game_state;
|
|
||||||
this.previousState = JSON.parse(JSON.stringify(data.game_state));
|
|
||||||
this.renderGame();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Legacy animation system - simplified
|
|
||||||
// Principle: State updates are instant, animations are fire-and-forget
|
|
||||||
// Exception: Local player's swap animation defers state until complete
|
// Exception: Local player's swap animation defers state until complete
|
||||||
|
|
||||||
// If local swap animation is running, defer this state update
|
// If local swap animation is running, defer this state update
|
||||||
@ -558,7 +353,6 @@ class GolfGame {
|
|||||||
|
|
||||||
// Render immediately with new state
|
// Render immediately with new state
|
||||||
this.renderGame();
|
this.renderGame();
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'your_turn':
|
case 'your_turn':
|
||||||
@ -820,16 +614,7 @@ class GolfGame {
|
|||||||
this.hideDrawnCard();
|
this.hideDrawnCard();
|
||||||
}
|
}
|
||||||
|
|
||||||
// New card system swap animation
|
// Animate player swapping drawn card with a card in their hand
|
||||||
animateSwapNew(position) {
|
|
||||||
if (!this.drawnCard) return;
|
|
||||||
|
|
||||||
// Send swap immediately - animation happens via state diff
|
|
||||||
this.send({ type: 'swap', position });
|
|
||||||
this.drawnCard = null;
|
|
||||||
this.hideDrawnCard();
|
|
||||||
}
|
|
||||||
|
|
||||||
animateSwap(position) {
|
animateSwap(position) {
|
||||||
const cardElements = this.playerCards.querySelectorAll('.card');
|
const cardElements = this.playerCards.querySelectorAll('.card');
|
||||||
const handCardEl = cardElements[position];
|
const handCardEl = cardElements[position];
|
||||||
@ -838,7 +623,7 @@ class GolfGame {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if card is already face-up (no flip needed)
|
// Check if card is already face-up
|
||||||
const myData = this.getMyPlayerData();
|
const myData = this.getMyPlayerData();
|
||||||
const card = myData?.cards[position];
|
const card = myData?.cards[position];
|
||||||
const isAlreadyFaceUp = card?.face_up;
|
const isAlreadyFaceUp = card?.face_up;
|
||||||
@ -850,6 +635,7 @@ class GolfGame {
|
|||||||
// Set up the animated card at hand position
|
// Set up the animated card at hand position
|
||||||
const swapCard = this.swapCardFromHand;
|
const swapCard = this.swapCardFromHand;
|
||||||
const swapCardFront = swapCard.querySelector('.swap-card-front');
|
const swapCardFront = swapCard.querySelector('.swap-card-front');
|
||||||
|
const swapCardInner = swapCard.querySelector('.swap-card-inner');
|
||||||
|
|
||||||
// Position at the hand card location
|
// Position at the hand card location
|
||||||
swapCard.style.left = handRect.left + 'px';
|
swapCard.style.left = handRect.left + 'px';
|
||||||
@ -862,8 +648,8 @@ class GolfGame {
|
|||||||
swapCardFront.innerHTML = '';
|
swapCardFront.innerHTML = '';
|
||||||
swapCardFront.className = 'swap-card-front';
|
swapCardFront.className = 'swap-card-front';
|
||||||
|
|
||||||
// If already face-up, show the card content immediately
|
|
||||||
if (isAlreadyFaceUp && card) {
|
if (isAlreadyFaceUp && card) {
|
||||||
|
// FACE-UP CARD: Show card content immediately, then slide to discard
|
||||||
if (card.rank === '★') {
|
if (card.rank === '★') {
|
||||||
swapCardFront.classList.add('joker');
|
swapCardFront.classList.add('joker');
|
||||||
const jokerIcon = card.suit === 'hearts' ? '🐉' : '👹';
|
const jokerIcon = card.suit === 'hearts' ? '🐉' : '👹';
|
||||||
@ -873,72 +659,76 @@ class GolfGame {
|
|||||||
const suitSymbol = { hearts: '♥', diamonds: '♦', clubs: '♣', spades: '♠' }[card.suit];
|
const suitSymbol = { hearts: '♥', diamonds: '♦', clubs: '♣', spades: '♠' }[card.suit];
|
||||||
swapCardFront.innerHTML = `${card.rank}<br>${suitSymbol}`;
|
swapCardFront.innerHTML = `${card.rank}<br>${suitSymbol}`;
|
||||||
}
|
}
|
||||||
swapCard.classList.add('flipping'); // Start showing front immediately
|
swapCard.classList.add('flipping'); // Show front immediately
|
||||||
}
|
|
||||||
|
|
||||||
// Hide the actual hand card
|
// Hide the actual hand card and discard
|
||||||
handCardEl.classList.add('swap-out');
|
handCardEl.classList.add('swap-out');
|
||||||
|
|
||||||
// Hide the discard (drawn card)
|
|
||||||
this.discard.classList.add('swap-to-hand');
|
this.discard.classList.add('swap-to-hand');
|
||||||
|
|
||||||
// Show animation overlay
|
|
||||||
this.swapAnimation.classList.remove('hidden');
|
this.swapAnimation.classList.remove('hidden');
|
||||||
|
|
||||||
// Mark that we're animating - defer game state renders
|
// Mark animating
|
||||||
this.swapAnimationInProgress = true;
|
this.swapAnimationInProgress = true;
|
||||||
this.swapAnimationCardEl = handCardEl;
|
this.swapAnimationCardEl = handCardEl;
|
||||||
this.swapAnimationFront = swapCardFront;
|
this.swapAnimationContentSet = true;
|
||||||
this.swapAnimationContentSet = isAlreadyFaceUp; // Skip updateSwapAnimation if we already set content
|
|
||||||
|
|
||||||
// Send swap immediately so server can respond
|
// Send swap
|
||||||
this.send({ type: 'swap', position });
|
this.send({ type: 'swap', position });
|
||||||
this.drawnCard = null;
|
this.drawnCard = null;
|
||||||
|
|
||||||
// Timing depends on whether we need to flip first
|
// Slide to discard
|
||||||
const flipDelay = isAlreadyFaceUp ? 0 : 450;
|
|
||||||
|
|
||||||
// Step 1: Flip the card over (only if face-down)
|
|
||||||
if (!isAlreadyFaceUp) {
|
|
||||||
setTimeout(() => {
|
|
||||||
swapCard.classList.add('flipping');
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Move to discard position
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
swapCard.classList.add('moving');
|
swapCard.classList.add('moving');
|
||||||
swapCard.style.left = discardRect.left + 'px';
|
swapCard.style.left = discardRect.left + 'px';
|
||||||
swapCard.style.top = discardRect.top + 'px';
|
swapCard.style.top = discardRect.top + 'px';
|
||||||
}, flipDelay + 50);
|
}, 50);
|
||||||
|
|
||||||
// Step 3: Card has landed - pause to show the card
|
// Complete
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
swapCard.classList.remove('moving');
|
|
||||||
}, flipDelay + 400);
|
|
||||||
|
|
||||||
// Step 4: Complete animation and render final state
|
|
||||||
setTimeout(() => {
|
|
||||||
// Hide animation overlay
|
|
||||||
this.swapAnimation.classList.add('hidden');
|
this.swapAnimation.classList.add('hidden');
|
||||||
swapCard.classList.remove('flipping', 'moving');
|
swapCard.classList.remove('flipping', 'moving');
|
||||||
|
|
||||||
// Reset card states
|
|
||||||
handCardEl.classList.remove('swap-out');
|
handCardEl.classList.remove('swap-out');
|
||||||
this.discard.classList.remove('swap-to-hand');
|
this.discard.classList.remove('swap-to-hand');
|
||||||
|
|
||||||
// Now allow renders and show the final state
|
|
||||||
this.swapAnimationInProgress = false;
|
this.swapAnimationInProgress = false;
|
||||||
this.swapAnimationContentSet = false;
|
|
||||||
this.hideDrawnCard();
|
this.hideDrawnCard();
|
||||||
|
|
||||||
// Render the pending game state if we have one
|
|
||||||
if (this.pendingGameState) {
|
if (this.pendingGameState) {
|
||||||
this.gameState = this.pendingGameState;
|
this.gameState = this.pendingGameState;
|
||||||
this.pendingGameState = null;
|
this.pendingGameState = null;
|
||||||
this.renderGame();
|
this.renderGame();
|
||||||
}
|
}
|
||||||
}, flipDelay + 900);
|
}, 500);
|
||||||
|
} else {
|
||||||
|
// FACE-DOWN CARD: Just slide card-back to discard (no flip mid-air)
|
||||||
|
// The new card will appear instantly when state updates
|
||||||
|
|
||||||
|
// Don't use overlay for face-down - just send swap and let state handle it
|
||||||
|
// This avoids the clunky "flip to empty front" issue
|
||||||
|
this.swapAnimationInProgress = true;
|
||||||
|
this.swapAnimationCardEl = handCardEl;
|
||||||
|
this.swapAnimationContentSet = false;
|
||||||
|
|
||||||
|
// Send swap
|
||||||
|
this.send({ type: 'swap', position });
|
||||||
|
this.drawnCard = null;
|
||||||
|
|
||||||
|
// Brief visual feedback - hide drawn card area
|
||||||
|
this.discard.classList.add('swap-to-hand');
|
||||||
|
handCardEl.classList.add('swap-out');
|
||||||
|
|
||||||
|
// Short timeout then let state update handle it
|
||||||
|
setTimeout(() => {
|
||||||
|
this.discard.classList.remove('swap-to-hand');
|
||||||
|
handCardEl.classList.remove('swap-out');
|
||||||
|
this.swapAnimationInProgress = false;
|
||||||
|
this.hideDrawnCard();
|
||||||
|
|
||||||
|
if (this.pendingGameState) {
|
||||||
|
this.gameState = this.pendingGameState;
|
||||||
|
this.pendingGameState = null;
|
||||||
|
this.renderGame();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the animated card with actual card content when server responds
|
// Update the animated card with actual card content when server responds
|
||||||
@ -1327,11 +1117,7 @@ class GolfGame {
|
|||||||
|
|
||||||
// Swap with drawn card
|
// Swap with drawn card
|
||||||
if (this.drawnCard) {
|
if (this.drawnCard) {
|
||||||
if (USE_NEW_CARD_SYSTEM) {
|
|
||||||
this.animateSwapNew(position);
|
|
||||||
} else {
|
|
||||||
this.animateSwap(position);
|
this.animateSwap(position);
|
||||||
}
|
|
||||||
this.hideToast();
|
this.hideToast();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1347,8 +1133,10 @@ class GolfGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextRound() {
|
nextRound() {
|
||||||
|
this.clearNextHoleCountdown();
|
||||||
this.send({ type: 'next_round' });
|
this.send({ type: 'next_round' });
|
||||||
this.gameButtons.classList.add('hidden');
|
this.gameButtons.classList.add('hidden');
|
||||||
|
this.nextRoundBtn.classList.remove('waiting');
|
||||||
}
|
}
|
||||||
|
|
||||||
newGame() {
|
newGame() {
|
||||||
@ -1387,10 +1175,6 @@ class GolfGame {
|
|||||||
this.isHost = false;
|
this.isHost = false;
|
||||||
this.gameState = null;
|
this.gameState = null;
|
||||||
this.previousState = null;
|
this.previousState = null;
|
||||||
// Clear card layer
|
|
||||||
if (USE_NEW_CARD_SYSTEM && this.cardManager) {
|
|
||||||
this.cardManager.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showWaitingRoom() {
|
showWaitingRoom() {
|
||||||
@ -1417,27 +1201,23 @@ class GolfGame {
|
|||||||
this.leaveGameBtn.textContent = this.isHost ? 'End Game' : 'Leave';
|
this.leaveGameBtn.textContent = this.isHost ? 'End Game' : 'Leave';
|
||||||
// Update active rules bar
|
// Update active rules bar
|
||||||
this.updateActiveRulesBar();
|
this.updateActiveRulesBar();
|
||||||
// Clear card layer for new card system
|
|
||||||
if (USE_NEW_CARD_SYSTEM && this.cardManager) {
|
|
||||||
this.cardManager.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateActiveRulesBar() {
|
updateActiveRulesBar() {
|
||||||
if (!this.gameState || !this.gameState.active_rules) {
|
if (!this.gameState) {
|
||||||
this.activeRulesBar.classList.add('hidden');
|
this.activeRulesBar.classList.add('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rules = this.gameState.active_rules;
|
const rules = this.gameState.active_rules || [];
|
||||||
if (rules.length === 0) {
|
if (rules.length === 0) {
|
||||||
this.activeRulesBar.classList.add('hidden');
|
// Show "Standard Rules" when no variants selected
|
||||||
return;
|
this.activeRulesList.innerHTML = '<span class="rule-tag standard">Standard</span>';
|
||||||
}
|
} else {
|
||||||
|
|
||||||
this.activeRulesList.innerHTML = rules
|
this.activeRulesList.innerHTML = rules
|
||||||
.map(rule => `<span class="rule-tag">${rule}</span>`)
|
.map(rule => `<span class="rule-tag">${rule}</span>`)
|
||||||
.join('');
|
.join('');
|
||||||
|
}
|
||||||
this.activeRulesBar.classList.remove('hidden');
|
this.activeRulesBar.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1526,11 +1306,17 @@ class GolfGame {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isFinalTurn = this.gameState.phase === 'final_turn';
|
||||||
const currentPlayer = this.gameState.players.find(p => p.id === this.gameState.current_player_id);
|
const currentPlayer = this.gameState.players.find(p => p.id === this.gameState.current_player_id);
|
||||||
|
|
||||||
if (currentPlayer && currentPlayer.id !== this.playerId) {
|
if (currentPlayer && currentPlayer.id !== this.playerId) {
|
||||||
this.setStatus(`${currentPlayer.name}'s turn`);
|
const prefix = isFinalTurn ? '⚡ Final turn: ' : '';
|
||||||
|
this.setStatus(`${prefix}${currentPlayer.name}'s turn`);
|
||||||
} else if (this.isMyTurn()) {
|
} else if (this.isMyTurn()) {
|
||||||
this.setStatus('Your turn - draw a card', 'your-turn');
|
const message = isFinalTurn
|
||||||
|
? '⚡ Final turn! Draw a card'
|
||||||
|
: 'Your turn - draw a card';
|
||||||
|
this.setStatus(message, 'your-turn');
|
||||||
} else {
|
} else {
|
||||||
this.setStatus('');
|
this.setStatus('');
|
||||||
}
|
}
|
||||||
@ -1646,11 +1432,16 @@ class GolfGame {
|
|||||||
const showingScore = this.calculateShowingScore(me.cards);
|
const showingScore = this.calculateShowingScore(me.cards);
|
||||||
this.yourScore.textContent = showingScore;
|
this.yourScore.textContent = showingScore;
|
||||||
|
|
||||||
|
// Check if player won the round
|
||||||
|
const isRoundWinner = this.roundWinnerNames.has(me.name);
|
||||||
|
this.playerArea.classList.toggle('round-winner', isRoundWinner);
|
||||||
|
|
||||||
// Update player name in header (truncate if needed)
|
// Update player name in header (truncate if needed)
|
||||||
const displayName = me.name.length > 12 ? me.name.substring(0, 11) + '…' : me.name;
|
const displayName = me.name.length > 12 ? me.name.substring(0, 11) + '…' : me.name;
|
||||||
const checkmark = me.all_face_up ? ' ✓' : '';
|
const checkmark = me.all_face_up ? ' ✓' : '';
|
||||||
|
const crownEmoji = isRoundWinner ? ' 👑' : '';
|
||||||
// Set text content before the score span
|
// Set text content before the score span
|
||||||
this.playerHeader.childNodes[0].textContent = displayName + checkmark;
|
this.playerHeader.childNodes[0].textContent = displayName + checkmark + crownEmoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update discard pile (skip if holding a drawn card)
|
// Update discard pile (skip if holding a drawn card)
|
||||||
@ -1708,25 +1499,21 @@ class GolfGame {
|
|||||||
div.classList.add('current-turn');
|
div.classList.add('current-turn');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRoundWinner = this.roundWinnerNames.has(player.name);
|
||||||
|
if (isRoundWinner) {
|
||||||
|
div.classList.add('round-winner');
|
||||||
|
}
|
||||||
|
|
||||||
const displayName = player.name.length > 12 ? player.name.substring(0, 11) + '…' : player.name;
|
const displayName = player.name.length > 12 ? player.name.substring(0, 11) + '…' : player.name;
|
||||||
const showingScore = this.calculateShowingScore(player.cards);
|
const showingScore = this.calculateShowingScore(player.cards);
|
||||||
|
const crownEmoji = isRoundWinner ? ' 👑' : '';
|
||||||
|
|
||||||
if (USE_NEW_CARD_SYSTEM) {
|
|
||||||
// Render empty slots - cards are in card-layer
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<h4>${displayName}${player.all_face_up ? ' ✓' : ''}<span class="opponent-showing">${showingScore}</span></h4>
|
<h4>${displayName}${player.all_face_up ? ' ✓' : ''}${crownEmoji}<span class="opponent-showing">${showingScore}</span></h4>
|
||||||
<div class="card-grid">
|
|
||||||
${player.cards.map((_, i) => `<div class="card-slot" data-player="${player.id}" data-position="${i}"></div>`).join('')}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
div.innerHTML = `
|
|
||||||
<h4>${displayName}${player.all_face_up ? ' ✓' : ''}<span class="opponent-showing">${showingScore}</span></h4>
|
|
||||||
<div class="card-grid">
|
<div class="card-grid">
|
||||||
${player.cards.map(card => this.renderCard(card, false, false)).join('')}
|
${player.cards.map(card => this.renderCard(card, false, false)).join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
|
||||||
|
|
||||||
this.opponentsRow.appendChild(div);
|
this.opponentsRow.appendChild(div);
|
||||||
});
|
});
|
||||||
@ -1736,29 +1523,6 @@ class GolfGame {
|
|||||||
if (myData) {
|
if (myData) {
|
||||||
this.playerCards.innerHTML = '';
|
this.playerCards.innerHTML = '';
|
||||||
|
|
||||||
if (USE_NEW_CARD_SYSTEM) {
|
|
||||||
// Render empty slots - cards are in card-layer
|
|
||||||
myData.cards.forEach((card, index) => {
|
|
||||||
const isClickable = (
|
|
||||||
(this.gameState.waiting_for_initial_flip && !card.face_up) ||
|
|
||||||
(this.drawnCard) ||
|
|
||||||
(this.waitingForFlip && !card.face_up)
|
|
||||||
);
|
|
||||||
const isSelected = this.selectedCards.includes(index);
|
|
||||||
|
|
||||||
const slotEl = document.createElement('div');
|
|
||||||
slotEl.className = 'card-slot';
|
|
||||||
slotEl.dataset.player = this.playerId;
|
|
||||||
slotEl.dataset.position = index;
|
|
||||||
if (isClickable) slotEl.classList.add('clickable');
|
|
||||||
if (isSelected) slotEl.classList.add('selected');
|
|
||||||
slotEl.addEventListener('click', () => this.handleCardClick(index));
|
|
||||||
this.playerCards.appendChild(slotEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update persistent card positions and states
|
|
||||||
this.updatePersistentCards();
|
|
||||||
} else {
|
|
||||||
myData.cards.forEach((card, index) => {
|
myData.cards.forEach((card, index) => {
|
||||||
// Check if this card was locally flipped (immediate feedback)
|
// Check if this card was locally flipped (immediate feedback)
|
||||||
const isLocallyFlipped = this.locallyFlippedCards.has(index);
|
const isLocallyFlipped = this.locallyFlippedCards.has(index);
|
||||||
@ -1781,7 +1545,6 @@ class GolfGame {
|
|||||||
this.playerCards.appendChild(cardEl.firstChild);
|
this.playerCards.appendChild(cardEl.firstChild);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Show flip prompt for initial flip
|
// Show flip prompt for initial flip
|
||||||
// Show flip prompt during initial flip phase
|
// Show flip prompt during initial flip phase
|
||||||
@ -1916,6 +1679,16 @@ class GolfGame {
|
|||||||
showScoreboard(scores, isFinal, rankings) {
|
showScoreboard(scores, isFinal, rankings) {
|
||||||
this.scoreTable.innerHTML = '';
|
this.scoreTable.innerHTML = '';
|
||||||
|
|
||||||
|
// Find round winner(s) - lowest round score (not total)
|
||||||
|
const roundScores = scores.map(s => s.score);
|
||||||
|
const minRoundScore = Math.min(...roundScores);
|
||||||
|
this.roundWinnerNames = new Set(
|
||||||
|
scores.filter(s => s.score === minRoundScore).map(s => s.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-render to show winner highlights
|
||||||
|
this.renderGame();
|
||||||
|
|
||||||
const minScore = Math.min(...scores.map(s => s.total || s.score || 0));
|
const minScore = Math.min(...scores.map(s => s.total || s.score || 0));
|
||||||
|
|
||||||
scores.forEach(score => {
|
scores.forEach(score => {
|
||||||
@ -1954,13 +1727,58 @@ class GolfGame {
|
|||||||
|
|
||||||
// Show game buttons
|
// Show game buttons
|
||||||
this.gameButtons.classList.remove('hidden');
|
this.gameButtons.classList.remove('hidden');
|
||||||
|
this.newGameBtn.classList.add('hidden');
|
||||||
if (this.isHost) {
|
|
||||||
this.nextRoundBtn.classList.remove('hidden');
|
this.nextRoundBtn.classList.remove('hidden');
|
||||||
this.newGameBtn.classList.add('hidden');
|
|
||||||
|
// Start countdown for next hole
|
||||||
|
this.startNextHoleCountdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
startNextHoleCountdown() {
|
||||||
|
// Clear any existing countdown
|
||||||
|
if (this.nextHoleCountdownInterval) {
|
||||||
|
clearInterval(this.nextHoleCountdownInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
const COUNTDOWN_SECONDS = 15;
|
||||||
|
let remaining = COUNTDOWN_SECONDS;
|
||||||
|
|
||||||
|
const updateButton = () => {
|
||||||
|
if (this.isHost) {
|
||||||
|
this.nextRoundBtn.textContent = `Next Hole (${remaining}s)`;
|
||||||
|
this.nextRoundBtn.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
this.nextRoundBtn.classList.add('hidden');
|
this.nextRoundBtn.textContent = `Next hole in ${remaining}s...`;
|
||||||
this.newGameBtn.classList.add('hidden');
|
this.nextRoundBtn.disabled = true;
|
||||||
|
this.nextRoundBtn.classList.add('waiting');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateButton();
|
||||||
|
|
||||||
|
this.nextHoleCountdownInterval = setInterval(() => {
|
||||||
|
remaining--;
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
clearInterval(this.nextHoleCountdownInterval);
|
||||||
|
this.nextHoleCountdownInterval = null;
|
||||||
|
|
||||||
|
// Auto-advance if host
|
||||||
|
if (this.isHost) {
|
||||||
|
this.nextRound();
|
||||||
|
} else {
|
||||||
|
this.nextRoundBtn.textContent = 'Waiting for host...';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateButton();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearNextHoleCountdown() {
|
||||||
|
if (this.nextHoleCountdownInterval) {
|
||||||
|
clearInterval(this.nextHoleCountdownInterval);
|
||||||
|
this.nextHoleCountdownInterval = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -205,17 +205,17 @@
|
|||||||
<div class="game-layout">
|
<div class="game-layout">
|
||||||
<div class="game-main">
|
<div class="game-main">
|
||||||
<div class="game-header">
|
<div class="game-header">
|
||||||
|
<div class="header-col header-col-left">
|
||||||
<div class="round-info">Hole <span id="current-round">1</span>/<span id="total-rounds">9</span></div>
|
<div class="round-info">Hole <span id="current-round">1</span>/<span id="total-rounds">9</span></div>
|
||||||
<div class="game-header-center">
|
|
||||||
<div id="active-rules-bar" class="active-rules-bar hidden">
|
<div id="active-rules-bar" class="active-rules-bar hidden">
|
||||||
<span class="rules-label">Rules:</span>
|
<span class="rules-label">Rules:</span>
|
||||||
<span id="active-rules-list" class="rules-list"></span>
|
<span id="active-rules-list" class="rules-list"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-status">
|
</div>
|
||||||
|
<div class="header-col header-col-center">
|
||||||
<div id="status-message" class="status-message"></div>
|
<div id="status-message" class="status-message"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="header-col header-col-right">
|
||||||
<div class="header-buttons">
|
|
||||||
<button id="mute-btn" class="mute-btn" title="Toggle sound">🔊</button>
|
<button id="mute-btn" class="mute-btn" title="Toggle sound">🔊</button>
|
||||||
<button id="leave-game-btn" class="btn btn-small btn-danger">Leave</button>
|
<button id="leave-game-btn" class="btn btn-small btn-danger">Leave</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -285,6 +285,23 @@ input::placeholder {
|
|||||||
color: #1a472a;
|
color: #1a472a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Pulsing glow for Next Hole button */
|
||||||
|
#next-round-btn:not(.hidden) {
|
||||||
|
animation: glow-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 5px rgba(244, 164, 96, 0.4),
|
||||||
|
0 0 10px rgba(244, 164, 96, 0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 15px rgba(244, 164, 96, 0.8),
|
||||||
|
0 0 30px rgba(244, 164, 96, 0.4),
|
||||||
|
0 0 45px rgba(244, 164, 96, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #1a472a;
|
color: #1a472a;
|
||||||
@ -314,6 +331,14 @@ input::placeholder {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Waiting state for non-host next hole button */
|
||||||
|
#next-round-btn.waiting {
|
||||||
|
animation: none;
|
||||||
|
background: rgba(244, 164, 96, 0.4);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 30px 0;
|
margin: 30px 0;
|
||||||
@ -462,9 +487,9 @@ input::placeholder {
|
|||||||
|
|
||||||
/* Game Screen */
|
/* Game Screen */
|
||||||
.game-header {
|
.game-header {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2fr 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: rgba(0,0,0,0.35);
|
background: rgba(0,0,0,0.35);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@ -473,29 +498,30 @@ input::placeholder {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-col {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-col-left {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-col-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-col-right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.game-header .round-info {
|
.game-header .round-info {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-header-center {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-header .turn-info {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #f4a460;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-header .header-buttons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#leave-game-btn {
|
#leave-game-btn {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
@ -521,7 +547,6 @@ input::placeholder {
|
|||||||
.active-rules-bar {
|
.active-rules-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
@ -550,6 +575,11 @@ input::placeholder {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.active-rules-bar .rule-tag.standard {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
/* Card Styles */
|
/* Card Styles */
|
||||||
.card {
|
.card {
|
||||||
width: clamp(65px, 5.5vw, 100px);
|
width: clamp(65px, 5.5vw, 100px);
|
||||||
@ -1027,15 +1057,15 @@ input::placeholder {
|
|||||||
box-shadow: 0 0 0 2px #f4a460;
|
box-shadow: 0 0 0 2px #f4a460;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toast Notification */
|
/* Round winner highlight */
|
||||||
/* Header status area */
|
.opponent-area.round-winner h4,
|
||||||
.header-status {
|
.player-area.round-winner h4 {
|
||||||
display: flex;
|
background: rgba(200, 255, 50, 0.7);
|
||||||
align-items: center;
|
box-shadow: 0 0 15px rgba(200, 255, 50, 0.9), 0 0 30px rgba(200, 255, 50, 0.5);
|
||||||
justify-content: center;
|
color: #0a2a10;
|
||||||
min-width: 200px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Status message in header */
|
||||||
.status-message {
|
.status-message {
|
||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
BIN
server/games.db
BIN
server/games.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user