Fix swap animation stutter and remove 1s server-side dead delay
- Remove unused card_revealed broadcast + 1s asyncio.sleep in swap handler (client never handled this message, causing pure dead wait before game_state) - Defer swap-out (opacity:0) on hand cards to onStart callback so overlay covers the card before hiding it — eliminates visual gap for all players - Defer heldCardFloating visibility hide to onStart — held card stays visible until animation overlay replaces it - Thread onStart callback through animateUnifiedSwap → _runUnifiedSwap Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5408867921
commit
ea34ddf8e4
@ -81,6 +81,7 @@ class GolfGame {
|
|||||||
this.initCardTooltips();
|
this.initCardTooltips();
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
this.initMobileDetection();
|
this.initMobileDetection();
|
||||||
|
this.initDesktopScorecard();
|
||||||
this.checkUrlParams();
|
this.checkUrlParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,9 +112,11 @@ class GolfGame {
|
|||||||
this.isMobile = e.matches;
|
this.isMobile = e.matches;
|
||||||
document.body.classList.toggle('mobile-portrait', e.matches);
|
document.body.classList.toggle('mobile-portrait', e.matches);
|
||||||
setAppHeight();
|
setAppHeight();
|
||||||
// Close any open drawers on layout change
|
// Close any open drawers/overlays on layout change
|
||||||
if (!e.matches) {
|
if (!e.matches) {
|
||||||
this.closeDrawers();
|
this.closeDrawers();
|
||||||
|
} else {
|
||||||
|
this.closeDesktopScorecard();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
mql.addEventListener('change', update);
|
mql.addEventListener('change', update);
|
||||||
@ -154,6 +157,31 @@ class GolfGame {
|
|||||||
if (bottomBar) bottomBar.classList.remove('hidden');
|
if (bottomBar) bottomBar.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initDesktopScorecard() {
|
||||||
|
if (!this.desktopScorecardBtn) return;
|
||||||
|
|
||||||
|
this.desktopScorecardBtn.addEventListener('click', () => {
|
||||||
|
const isOpen = this.desktopScorecardOverlay.classList.contains('open');
|
||||||
|
if (isOpen) {
|
||||||
|
this.closeDesktopScorecard();
|
||||||
|
} else {
|
||||||
|
this.desktopScorecardOverlay.classList.add('open');
|
||||||
|
this.desktopScorecardBtn.classList.add('active');
|
||||||
|
this.desktopScorecardBackdrop.classList.add('visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.desktopScorecardBackdrop.addEventListener('click', () => {
|
||||||
|
this.closeDesktopScorecard();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDesktopScorecard() {
|
||||||
|
if (this.desktopScorecardOverlay) this.desktopScorecardOverlay.classList.remove('open');
|
||||||
|
if (this.desktopScorecardBtn) this.desktopScorecardBtn.classList.remove('active');
|
||||||
|
if (this.desktopScorecardBackdrop) this.desktopScorecardBackdrop.classList.remove('visible');
|
||||||
|
}
|
||||||
|
|
||||||
initAudio() {
|
initAudio() {
|
||||||
// Initialize audio context on first user interaction
|
// Initialize audio context on first user interaction
|
||||||
const initCtx = () => {
|
const initCtx = () => {
|
||||||
@ -544,6 +572,13 @@ class GolfGame {
|
|||||||
this.gameUsername = document.getElementById('game-username');
|
this.gameUsername = document.getElementById('game-username');
|
||||||
this.gameLogoutBtn = document.getElementById('game-logout-btn');
|
this.gameLogoutBtn = document.getElementById('game-logout-btn');
|
||||||
this.authBar = document.getElementById('auth-bar');
|
this.authBar = document.getElementById('auth-bar');
|
||||||
|
|
||||||
|
// Desktop scorecard overlay elements
|
||||||
|
this.desktopScorecardBtn = document.getElementById('desktop-scorecard-btn');
|
||||||
|
this.desktopScorecardOverlay = document.getElementById('desktop-scorecard-overlay');
|
||||||
|
this.desktopScorecardBackdrop = document.getElementById('desktop-scorecard-backdrop');
|
||||||
|
this.desktopStandingsList = document.getElementById('desktop-standings-list');
|
||||||
|
this.desktopScoreTable = document.getElementById('desktop-score-table')?.querySelector('tbody');
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
@ -1464,12 +1499,8 @@ class GolfGame {
|
|||||||
this.swapAnimationCardEl = handCardEl;
|
this.swapAnimationCardEl = handCardEl;
|
||||||
this.swapAnimationHandCardEl = handCardEl;
|
this.swapAnimationHandCardEl = handCardEl;
|
||||||
|
|
||||||
// Hide originals and UI during animation
|
// Hide discard button during animation (held card hidden later by onStart)
|
||||||
handCardEl.classList.add('swap-out');
|
|
||||||
this.discardBtn.classList.add('hidden');
|
this.discardBtn.classList.add('hidden');
|
||||||
if (this.heldCardFloating) {
|
|
||||||
this.heldCardFloating.style.visibility = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store drawn card data before clearing
|
// Store drawn card data before clearing
|
||||||
const drawnCardData = this.drawnCard;
|
const drawnCardData = this.drawnCard;
|
||||||
@ -1492,6 +1523,12 @@ class GolfGame {
|
|||||||
{
|
{
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
wasHandFaceDown: false,
|
wasHandFaceDown: false,
|
||||||
|
onStart: () => {
|
||||||
|
handCardEl.classList.add('swap-out');
|
||||||
|
if (this.heldCardFloating) {
|
||||||
|
this.heldCardFloating.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
handCardEl.classList.remove('swap-out');
|
handCardEl.classList.remove('swap-out');
|
||||||
if (this.heldCardFloating) {
|
if (this.heldCardFloating) {
|
||||||
@ -1555,6 +1592,12 @@ class GolfGame {
|
|||||||
{
|
{
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
wasHandFaceDown: true,
|
wasHandFaceDown: true,
|
||||||
|
onStart: () => {
|
||||||
|
if (handCardEl) handCardEl.classList.add('swap-out');
|
||||||
|
if (this.heldCardFloating) {
|
||||||
|
this.heldCardFloating.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
if (handCardEl) handCardEl.classList.remove('swap-out');
|
if (handCardEl) handCardEl.classList.remove('swap-out');
|
||||||
if (this.heldCardFloating) {
|
if (this.heldCardFloating) {
|
||||||
@ -2887,9 +2930,6 @@ class GolfGame {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the source card during animation
|
|
||||||
sourceCardEl.classList.add('swap-out');
|
|
||||||
|
|
||||||
// Use unified swap animation
|
// Use unified swap animation
|
||||||
if (window.cardAnimations) {
|
if (window.cardAnimations) {
|
||||||
const heldRect = window.cardAnimations.getHoldingRect();
|
const heldRect = window.cardAnimations.getHoldingRect();
|
||||||
@ -2902,6 +2942,9 @@ class GolfGame {
|
|||||||
{
|
{
|
||||||
rotation: sourceRotation,
|
rotation: sourceRotation,
|
||||||
wasHandFaceDown: !wasFaceUp,
|
wasHandFaceDown: !wasFaceUp,
|
||||||
|
onStart: () => {
|
||||||
|
sourceCardEl.classList.add('swap-out');
|
||||||
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
if (sourceCardEl) sourceCardEl.classList.remove('swap-out');
|
if (sourceCardEl) sourceCardEl.classList.remove('swap-out');
|
||||||
this.opponentSwapAnimation = null;
|
this.opponentSwapAnimation = null;
|
||||||
@ -4347,6 +4390,11 @@ class GolfGame {
|
|||||||
`;
|
`;
|
||||||
this.scoreTable.appendChild(tr);
|
this.scoreTable.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mirror to desktop overlay
|
||||||
|
if (this.desktopScoreTable) {
|
||||||
|
this.desktopScoreTable.innerHTML = this.scoreTable.innerHTML;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStandings() {
|
updateStandings() {
|
||||||
@ -4384,7 +4432,7 @@ class GolfGame {
|
|||||||
return `<div class="rank-row ${holesRank === 0 && p.rounds_won > 0 ? 'leader' : ''}"><span class="rank-pos">${medal}</span><span class="rank-name">${name}</span><span class="rank-val">${p.rounds_won} wins</span></div>`;
|
return `<div class="rank-row ${holesRank === 0 && p.rounds_won > 0 ? 'leader' : ''}"><span class="rank-pos">${medal}</span><span class="rank-name">${name}</span><span class="rank-val">${p.rounds_won} wins</span></div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
this.standingsList.innerHTML = `
|
const standingsContent = `
|
||||||
<div class="standings-section">
|
<div class="standings-section">
|
||||||
<div class="standings-title">By Score</div>
|
<div class="standings-title">By Score</div>
|
||||||
${pointsHtml}
|
${pointsHtml}
|
||||||
@ -4394,6 +4442,10 @@ class GolfGame {
|
|||||||
${holesHtml}
|
${holesHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
this.standingsList.innerHTML = standingsContent;
|
||||||
|
if (this.desktopStandingsList) {
|
||||||
|
this.desktopStandingsList.innerHTML = standingsContent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCard(card, clickable, selected) {
|
renderCard(card, clickable, selected) {
|
||||||
@ -4473,6 +4525,11 @@ class GolfGame {
|
|||||||
this.scoreTable.appendChild(tr);
|
this.scoreTable.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mirror to desktop overlay
|
||||||
|
if (this.desktopScoreTable) {
|
||||||
|
this.desktopScoreTable.innerHTML = this.scoreTable.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
// Show rankings announcement only for final results
|
// Show rankings announcement only for final results
|
||||||
const existingAnnouncement = document.getElementById('rankings-announcement');
|
const existingAnnouncement = document.getElementById('rankings-announcement');
|
||||||
if (existingAnnouncement) existingAnnouncement.remove();
|
if (existingAnnouncement) existingAnnouncement.remove();
|
||||||
|
|||||||
@ -1105,7 +1105,7 @@ class CardAnimations {
|
|||||||
// heldRect: position of the held card (or null to use default holding position)
|
// heldRect: position of the held card (or null to use default holding position)
|
||||||
// options: { rotation, wasHandFaceDown, onComplete }
|
// options: { rotation, wasHandFaceDown, onComplete }
|
||||||
animateUnifiedSwap(handCardData, heldCardData, handRect, heldRect, options = {}) {
|
animateUnifiedSwap(handCardData, heldCardData, handRect, heldRect, options = {}) {
|
||||||
const { rotation = 0, wasHandFaceDown = false, onComplete } = options;
|
const { rotation = 0, wasHandFaceDown = false, onComplete, onStart } = options;
|
||||||
const T = window.TIMING?.swap || { lift: 100, arc: 320, settle: 100 };
|
const T = window.TIMING?.swap || { lift: 100, arc: 320, settle: 100 };
|
||||||
const discardRect = this.getDiscardRect();
|
const discardRect = this.getDiscardRect();
|
||||||
|
|
||||||
@ -1137,15 +1137,15 @@ class CardAnimations {
|
|||||||
delete el.dataset.animating;
|
delete el.dataset.animating;
|
||||||
el.remove();
|
el.remove();
|
||||||
});
|
});
|
||||||
this._runUnifiedSwap(handCardData, heldCardData, handRect, heldRect, discardRect, T, rotation, wasHandFaceDown, onComplete);
|
this._runUnifiedSwap(handCardData, heldCardData, handRect, heldRect, discardRect, T, rotation, wasHandFaceDown, onComplete, onStart);
|
||||||
}, 350);
|
}, 350);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._runUnifiedSwap(handCardData, heldCardData, handRect, heldRect, discardRect, T, rotation, wasHandFaceDown, onComplete);
|
this._runUnifiedSwap(handCardData, heldCardData, handRect, heldRect, discardRect, T, rotation, wasHandFaceDown, onComplete, onStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
_runUnifiedSwap(handCardData, heldCardData, handRect, heldRect, discardRect, T, rotation, wasHandFaceDown, onComplete) {
|
_runUnifiedSwap(handCardData, heldCardData, handRect, heldRect, discardRect, T, rotation, wasHandFaceDown, onComplete, onStart) {
|
||||||
// Create the two traveling cards
|
// Create the two traveling cards
|
||||||
const travelingHand = this.createCardFromData(handCardData, handRect, rotation);
|
const travelingHand = this.createCardFromData(handCardData, handRect, rotation);
|
||||||
const travelingHeld = this.createCardFromData(heldCardData, heldRect, 0);
|
const travelingHeld = this.createCardFromData(heldCardData, heldRect, 0);
|
||||||
@ -1154,6 +1154,9 @@ class CardAnimations {
|
|||||||
document.body.appendChild(travelingHand);
|
document.body.appendChild(travelingHand);
|
||||||
document.body.appendChild(travelingHeld);
|
document.body.appendChild(travelingHeld);
|
||||||
|
|
||||||
|
// Now that overlays cover the originals, hide them
|
||||||
|
if (onStart) onStart();
|
||||||
|
|
||||||
this.playSound('card');
|
this.playSound('card');
|
||||||
|
|
||||||
// If hand card was face-down, flip it first
|
// If hand card was face-down, flip it first
|
||||||
|
|||||||
@ -313,22 +313,6 @@ async def handle_swap(data: dict, ctx: ConnectionContext, *, broadcast_game_stat
|
|||||||
reason=f"swapped {drawn_card.rank.value} into position {position}, replaced {old_rank}",
|
reason=f"swapped {drawn_card.rank.value} into position {position}, replaced {old_rank}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Broadcast reveal of old face-down card before state update
|
|
||||||
if old_card_data:
|
|
||||||
reveal_msg = {
|
|
||||||
"type": "card_revealed",
|
|
||||||
"player_id": ctx.player_id,
|
|
||||||
"position": position,
|
|
||||||
"card": old_card_data,
|
|
||||||
}
|
|
||||||
for pid, p in ctx.current_room.players.items():
|
|
||||||
if not p.is_cpu and p.websocket:
|
|
||||||
try:
|
|
||||||
await p.websocket.send_json(reveal_msg)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
await asyncio.sleep(1.0)
|
|
||||||
|
|
||||||
await broadcast_game_state(ctx.current_room)
|
await broadcast_game_state(ctx.current_room)
|
||||||
await asyncio.sleep(1.0)
|
await asyncio.sleep(1.0)
|
||||||
check_and_run_cpu_turn(ctx.current_room)
|
check_and_run_cpu_turn(ctx.current_room)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user