add animations, mobile responsiveness, and drag-and-drop improvements
This commit is contained in:
parent
65f74bd0c6
commit
f28b0fa62e
@ -5,7 +5,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Klondike Solitaire</title>
|
||||
<link rel="stylesheet" href="style.css"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
|
||||
347
script.js
347
script.js
@ -10,11 +10,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
let deck, stock, waste, foundations, tableau;
|
||||
let cardElements = {};
|
||||
let dragged = { cards: [], sourcePile: null, startIndex: -1 };
|
||||
// 'selected' state is no longer needed for click controls and has been removed.
|
||||
let dragged = { cards: [], sourcePile: null, startIndex: -1, ghostEl: null, offsetX: 0, offsetY: 0 };
|
||||
const history = [];
|
||||
|
||||
let isAutoCompleting = false;
|
||||
let autoCompleteInterval = null;
|
||||
let isDealing = false;
|
||||
let score = 0;
|
||||
let highScore = 0;
|
||||
|
||||
@ -65,6 +66,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
history.push(...gameState.history);
|
||||
DRAW_COUNT = gameState.drawCount || 3;
|
||||
score = gameState.score || 0;
|
||||
highScore = parseInt(localStorage.getItem('solitaire-high-score'), 10) || 0;
|
||||
drawSelect.value = DRAW_COUNT;
|
||||
Object.values(cardElements).forEach(el => el.remove());
|
||||
cardElements = {};
|
||||
@ -102,10 +104,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// ====== SETUP ======
|
||||
function initState() {
|
||||
score = 0;
|
||||
highScore = localStorage.getItem('solitaire-high-score') || 0;
|
||||
highScore = parseInt(localStorage.getItem('solitaire-high-score'), 10) || 0;
|
||||
updateScoreDisplay();
|
||||
|
||||
isAutoCompleting = false;
|
||||
if (autoCompleteInterval) clearInterval(autoCompleteInterval);
|
||||
|
||||
Object.values(cardElements).forEach(el => el.remove());
|
||||
cardElements = {};
|
||||
deck = []; stock = []; waste = [];
|
||||
@ -122,16 +126,54 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const el = createCardElement(cardData);
|
||||
cardElements[cardData.id] = el;
|
||||
gameBoard.appendChild(el);
|
||||
// Initially position cards at the stock location to animate out from there
|
||||
const stockEl = document.getElementById('stock');
|
||||
if (stockEl) {
|
||||
const rect = stockEl.getBoundingClientRect();
|
||||
const boardRect = gameBoard.getBoundingClientRect();
|
||||
el.style.top = `${rect.top - boardRect.top + PILE_BORDER_WIDTH}px`;
|
||||
el.style.left = `${rect.left - boardRect.left + PILE_BORDER_WIDTH}px`;
|
||||
}
|
||||
});
|
||||
|
||||
// Separate cards for tableau and stock
|
||||
const tableauCards = [];
|
||||
for (let i=0; i<7; i++){
|
||||
for (let j=i; j<7; j++) tableau[j].push(deck.pop());
|
||||
for (let j=i; j<7; j++) {
|
||||
const card = deck.pop();
|
||||
tableau[j].push(card);
|
||||
tableauCards.push({ card, pileIndex: j });
|
||||
}
|
||||
}
|
||||
tableau.forEach(pile => { if (pile.length) pile[pile.length-1].faceUp = true; });
|
||||
stock = deck;
|
||||
|
||||
winMessage.classList.add('hidden');
|
||||
history.length = 0;
|
||||
pushHistory();
|
||||
updateBoard(true);
|
||||
|
||||
// Start dealing animation
|
||||
dealTableauSequentially(tableauCards);
|
||||
}
|
||||
|
||||
async function dealTableauSequentially(cardsToDeal) {
|
||||
isDealing = true;
|
||||
// Turn off interactivity during deal
|
||||
gameBoard.style.pointerEvents = 'none';
|
||||
|
||||
for (let i = 0; i < cardsToDeal.length; i++) {
|
||||
const { card, pileIndex } = cardsToDeal[i];
|
||||
// Only flip the last card of each pile
|
||||
const pile = tableau[pileIndex];
|
||||
if (card === pile[pile.length - 1]) {
|
||||
card.faceUp = true;
|
||||
}
|
||||
updateBoard(true);
|
||||
await new Promise(r => setTimeout(r, 60));
|
||||
}
|
||||
|
||||
isDealing = false;
|
||||
gameBoard.style.pointerEvents = 'auto';
|
||||
updateBoard();
|
||||
saveGame();
|
||||
}
|
||||
|
||||
@ -148,6 +190,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
pileData.forEach((cardData, i) => {
|
||||
const cardEl = cardElements[cardData.id];
|
||||
if(!cardEl) return;
|
||||
// Don't update position for cards being dragged (ghost)
|
||||
if (dragged.ghostEl && dragged.cards.some(c => c.id === cardData.id)) {
|
||||
cardEl.style.visibility = 'hidden'; // Hide the original card
|
||||
return;
|
||||
} else {
|
||||
cardEl.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
if (type === 'tableau') {
|
||||
cardEl.style.top = `${baseTop + currentOffset}px`;
|
||||
currentOffset += cardData.faceUp ? SPACING_FACE_UP : SPACING_FACE_DOWN;
|
||||
@ -164,7 +214,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
cardEl.classList.remove('is-flipped');
|
||||
cardEl.draggable = false;
|
||||
}
|
||||
// The 'selected' class is no longer applied via click.
|
||||
const zBase = type === 'tableau' ? 100 : type === 'foundation' ? 400 : 600;
|
||||
cardEl.style.zIndex = zBase + i;
|
||||
});
|
||||
@ -181,6 +230,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
waste.forEach((cardData, i) => {
|
||||
const cardEl = cardElements[cardData.id];
|
||||
if(!cardEl) return;
|
||||
|
||||
if (dragged.ghostEl && dragged.cards.some(c => c.id === cardData.id)) {
|
||||
cardEl.style.visibility = 'hidden';
|
||||
return;
|
||||
} else {
|
||||
cardEl.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
const isTopCard = i === waste.length - 1;
|
||||
const visibleFanIndexStart = Math.max(0, waste.length - DRAW_COUNT);
|
||||
let leftOffset = 0;
|
||||
@ -266,30 +323,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (type === 'stock') return stock;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper to get the actual pile that a card belongs to
|
||||
function getPileContainingCard(cardId) {
|
||||
const checkPile = (pile) => pile.find(c => c.id === cardId);
|
||||
|
||||
if (checkPile(waste)) return waste;
|
||||
for (const pile of tableau) {
|
||||
if (checkPile(pile)) return pile;
|
||||
}
|
||||
for (const pile of foundations) {
|
||||
if (checkPile(pile)) return pile;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findCardData(cardEl) {
|
||||
const id = cardEl.dataset.id;
|
||||
let cardIndex;
|
||||
const findIn = (pile) => pile.findIndex(c => c.id === id);
|
||||
if ((cardIndex = findIn(waste)) > -1 && cardIndex === waste.length - 1) {
|
||||
return { sourcePile: waste, cardIndex, card: waste[cardIndex], cardsToMove: [waste[cardIndex]] };
|
||||
|
||||
// Check waste (only top card can be moved)
|
||||
if (waste.length > 0 && waste[waste.length - 1].id === id) {
|
||||
return { sourcePile: waste, cardIndex: waste.length - 1, card: waste[waste.length - 1], cardsToMove: [waste[waste.length - 1]] };
|
||||
}
|
||||
|
||||
// Check tableau piles (any face-up card and cards above it can be moved)
|
||||
for (const pile of tableau) {
|
||||
if ((cardIndex = findIn(pile)) > -1 && pile[cardIndex].faceUp) {
|
||||
cardIndex = pile.findIndex(c => c.id === id);
|
||||
if (cardIndex > -1 && pile[cardIndex].faceUp) {
|
||||
const cardsToMove = pile.slice(cardIndex);
|
||||
return { sourcePile: pile, cardIndex, card: cardsToMove[0], cardsToMove };
|
||||
}
|
||||
}
|
||||
// Check foundations (only top card can be moved)
|
||||
for (const pile of foundations) {
|
||||
if ((cardIndex = findIn(pile)) > -1 && cardIndex === pile.length - 1) {
|
||||
return { sourcePile: pile, cardIndex, card: pile[cardIndex], cardsToMove: [pile[cardIndex]] };
|
||||
if (pile.length > 0 && pile[pile.length - 1].id === id) {
|
||||
return { sourcePile: pile, cardIndex: pile.length - 1, card: pile[pile.length - 1], cardsToMove: [pile[pile.length - 1]] };
|
||||
}
|
||||
}
|
||||
return {};
|
||||
return {}; // Return empty object if card not found or not movable
|
||||
}
|
||||
|
||||
function canMove(card, destPile) {
|
||||
if (!card || !destPile) return false;
|
||||
|
||||
// Prevent moving to the stock pile
|
||||
if (destPile === stock) return false;
|
||||
|
||||
const destType = (foundations.includes(destPile)) ? 'foundation' : (tableau.includes(destPile)) ? 'tableau' : 'other';
|
||||
if (destType === 'other') return false;
|
||||
if (destType === 'other' && destPile !== waste) return false; // Only allow tableau, foundation, or waste as actual destinations
|
||||
|
||||
// Waste pile can only receive cards if it's not the source for the current drag (handled by dragged.sourcePile check)
|
||||
// Also, waste doesn't typically accept cards from other piles in Klondike.
|
||||
// We ensure we don't try to drop on the waste pile itself unless it's for internal logic like recycling stock.
|
||||
if (destPile === waste) return false; // Generally, cards don't move TO waste.
|
||||
|
||||
if (destPile.length === 0) {
|
||||
return (destType==='tableau' && card.value==='K') || (destType==='foundation' && card.value==='A');
|
||||
}
|
||||
@ -297,13 +385,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (destType === 'tableau') {
|
||||
const opposite = ((['♥','♦'].includes(card.suit) && ['♣','♠'].includes(top.suit)) || (['♣','♠'].includes(card.suit) && ['♥','♦'].includes(top.suit)));
|
||||
return opposite && getValueRank(card.value) === getValueRank(top.value) - 1;
|
||||
} else {
|
||||
} else if (destType === 'foundation') {
|
||||
return card.suit === top.suit && getValueRank(card.value) === getValueRank(top.value) + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The `clearSelection` function is no longer needed.
|
||||
|
||||
function moveSequence(sourcePile, startIndex, destPile) {
|
||||
const seq = sourcePile.splice(startIndex);
|
||||
destPile.push(...seq);
|
||||
@ -330,19 +417,92 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateScoreDisplay();
|
||||
}
|
||||
winMessage.classList.remove('hidden');
|
||||
try {
|
||||
confetti({ particleCount: 180, spread: 75, origin:{y:0.35} });
|
||||
setTimeout(()=>confetti({ particleCount: 140, spread: 65, origin:{y:.2} }), 350);
|
||||
} catch(_) {}
|
||||
triggerBouncingCards();
|
||||
}
|
||||
}
|
||||
|
||||
function triggerBouncingCards() {
|
||||
const boardRect = gameBoard.getBoundingClientRect();
|
||||
const cardsToBounce = [];
|
||||
|
||||
// Gather all top cards from foundations to start the bounce sequence
|
||||
// We'll process them one suit at a time for the classic effect
|
||||
const animationFoundations = foundations.map(f => [...f]); // Clone
|
||||
|
||||
function bounceNextCard() {
|
||||
let cardFound = false;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (animationFoundations[i].length > 0) {
|
||||
const cardData = animationFoundations[i].pop();
|
||||
const cardEl = cardElements[cardData.id];
|
||||
if (cardEl) {
|
||||
startCardPhysics(cardEl);
|
||||
cardFound = true;
|
||||
break; // One card per trigger
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cardFound) {
|
||||
setTimeout(bounceNextCard, 180);
|
||||
}
|
||||
}
|
||||
|
||||
function startCardPhysics(el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
let x = rect.left;
|
||||
let y = rect.top;
|
||||
let vx = (Math.random() * 4 + 2) * (Math.random() > 0.5 ? 1 : -1);
|
||||
let vy = Math.random() * -5 - 2;
|
||||
const gravity = 0.5;
|
||||
const friction = 0.75;
|
||||
|
||||
// Remove transition for physics
|
||||
el.style.transition = 'none';
|
||||
el.style.position = 'fixed';
|
||||
el.style.zIndex = 3000;
|
||||
el.style.pointerEvents = 'none';
|
||||
|
||||
function step() {
|
||||
x += vx;
|
||||
y += vy;
|
||||
vy += gravity;
|
||||
|
||||
if (y + rect.height > window.innerHeight) {
|
||||
y = window.innerHeight - rect.height;
|
||||
vy *= -friction;
|
||||
if (Math.abs(vy) < 2) vy = 0; // Stop vertical bounce if too small
|
||||
}
|
||||
|
||||
if (x + rect.width < 0 || x > window.innerWidth) {
|
||||
el.remove();
|
||||
return; // Card off screen
|
||||
}
|
||||
|
||||
el.style.left = `${x}px`;
|
||||
el.style.top = `${y}px`;
|
||||
|
||||
if (Math.abs(vx) > 0.1 || Math.abs(vy) > 0.1 || y < window.innerHeight - rect.height) {
|
||||
requestAnimationFrame(step);
|
||||
} else {
|
||||
// Small slide before stopping
|
||||
vx *= 0.95;
|
||||
if (Math.abs(vx) > 0.1) requestAnimationFrame(step);
|
||||
else el.remove();
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
bounceNextCard();
|
||||
}
|
||||
|
||||
// ====== ACTIONS ======
|
||||
function handleStockClick() {
|
||||
if (isAutoCompleting) return;
|
||||
pushHistory();
|
||||
if (stock.length > 0) {
|
||||
const cardsToMove = stock.splice(stock.length - DRAW_COUNT, DRAW_COUNT).reverse();
|
||||
const drawCount = Math.min(stock.length, DRAW_COUNT);
|
||||
const cardsToMove = stock.splice(stock.length - drawCount, drawCount).reverse();
|
||||
cardsToMove.forEach(c => c.faceUp = true);
|
||||
waste.push(...cardsToMove);
|
||||
} else if (waste.length > 0) {
|
||||
@ -355,10 +515,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
saveGame();
|
||||
}
|
||||
|
||||
/**
|
||||
* This new function handles the single-click auto-move logic.
|
||||
* It finds the best valid move for a card and executes it.
|
||||
*/
|
||||
function performAutoMove(cardEl) {
|
||||
if (isAutoCompleting) return;
|
||||
const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl);
|
||||
@ -374,7 +530,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
pushHistory();
|
||||
moveSequence(sourcePile, cardIndex, f);
|
||||
performMoveCleanup(sourcePile);
|
||||
return; // Move executed, action is complete.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -388,7 +544,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
pushHistory();
|
||||
moveSequence(sourcePile, cardIndex, t);
|
||||
performMoveCleanup(sourcePile);
|
||||
return; // Move executed, action is complete.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -422,7 +578,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
isAutoCompleting = true;
|
||||
const intervalId = setInterval(() => {
|
||||
autoCompleteInterval = setInterval(() => {
|
||||
let movedCard = false;
|
||||
let source = null;
|
||||
|
||||
@ -460,7 +616,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (source) performMoveCleanup(source);
|
||||
|
||||
if (!movedCard) {
|
||||
clearInterval(intervalId);
|
||||
clearInterval(autoCompleteInterval);
|
||||
autoCompleteInterval = null;
|
||||
isAutoCompleting = false;
|
||||
checkWin();
|
||||
}
|
||||
@ -479,7 +636,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for(const f of foundations) if(canMove(topCard, f)) return true;
|
||||
|
||||
const movableStackIndex = sourcePile.findIndex(card => card.faceUp);
|
||||
if (movableStackIndex > -1) { // Fixed: check should be > -1
|
||||
if (movableStackIndex > -1) {
|
||||
const cardToMove = sourcePile[movableStackIndex];
|
||||
for (const destPile of tableau) {
|
||||
if (sourcePile !== destPile && canMove(cardToMove, destPile)) return true;
|
||||
@ -562,31 +719,147 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Clicks on anything else (background, empty piles, etc.) will do nothing.
|
||||
});
|
||||
|
||||
// Custom drag and drop functionality
|
||||
gameBoard.addEventListener('dragstart', e => {
|
||||
if (isAutoCompleting) { e.preventDefault(); return; }
|
||||
const cardEl = e.target.closest('.card');
|
||||
if (!cardEl) { e.preventDefault(); return; }
|
||||
|
||||
const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl);
|
||||
if (!sourcePile || !cardsToMove.length) { e.preventDefault(); return; }
|
||||
dragged = { cards: cardsToMove, sourcePile, startIndex: cardIndex };
|
||||
setTimeout(() => cardsToMove.forEach(c => cardElements[c.id]?.classList.add('dragging')), 0);
|
||||
|
||||
const rect = cardEl.getBoundingClientRect();
|
||||
dragged.offsetX = e.clientX - rect.left;
|
||||
dragged.offsetY = e.clientY - rect.top;
|
||||
dragged.cards = cardsToMove;
|
||||
dragged.sourcePile = sourcePile;
|
||||
dragged.startIndex = cardIndex;
|
||||
|
||||
// Create a "ghost" element for visual feedback
|
||||
dragged.ghostEl = document.createElement('div');
|
||||
dragged.ghostEl.classList.add('card-ghost-stack');
|
||||
// Add classes for card back if applicable
|
||||
const currentBackClass = document.body.className.match(/card-back-\w+/);
|
||||
if (currentBackClass) {
|
||||
dragged.ghostEl.classList.add(currentBackClass[0]);
|
||||
}
|
||||
|
||||
cardsToMove.forEach((cardData, i) => {
|
||||
const el = cardElements[cardData.id].cloneNode(true);
|
||||
el.style.position = 'absolute'; // Position relative to ghostEl
|
||||
el.style.top = `${i * SPACING_FACE_UP}px`;
|
||||
el.style.left = '0';
|
||||
el.style.zIndex = i + 1; // Stack visually
|
||||
el.classList.add('is-flipped'); // Ensure ghost cards are flipped
|
||||
el.style.visibility = 'visible';
|
||||
dragged.ghostEl.appendChild(el);
|
||||
});
|
||||
|
||||
dragged.ghostEl.style.position = 'fixed';
|
||||
dragged.ghostEl.style.pointerEvents = 'none'; // Ensure it doesn't interfere with drop targets
|
||||
dragged.ghostEl.style.zIndex = 2000; // Above everything else
|
||||
dragged.ghostEl.style.width = cardEl.offsetWidth + 'px'; // Match card width
|
||||
dragged.ghostEl.style.height = (cardEl.offsetHeight + (cardsToMove.length - 1) * SPACING_FACE_UP) + 'px';
|
||||
|
||||
document.body.appendChild(dragged.ghostEl);
|
||||
|
||||
// Hide the original cards visually while dragging
|
||||
cardsToMove.forEach(c => cardElements[c.id].style.visibility = 'hidden');
|
||||
|
||||
// Set custom drag image (a tiny transparent image) to prevent default ghost
|
||||
const img = new Image();
|
||||
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
e.dataTransfer.setDragImage(img, 0, 0);
|
||||
|
||||
// Initial position for the ghost
|
||||
dragged.ghostEl.style.left = `${e.clientX - dragged.offsetX}px`;
|
||||
dragged.ghostEl.style.top = `${e.clientY - dragged.offsetY}px`;
|
||||
});
|
||||
gameBoard.addEventListener('dragover', e => { e.preventDefault(); document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over')); const targetEl = e.target.closest('.pile, .card'); if (targetEl) targetEl.closest('.pile')?.classList.add('drag-over'); });
|
||||
gameBoard.addEventListener('dragend', () => { if (dragged.cards?.length) { dragged.cards.forEach(c => cardElements[c.id]?.classList.remove('dragging')); } dragged = { cards: [], sourcePile: null, startIndex: -1 }; document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over')); });
|
||||
|
||||
gameBoard.addEventListener('drag', e => {
|
||||
if (dragged.ghostEl && e.clientX !== 0) {
|
||||
// Update ghost element position to follow the cursor
|
||||
dragged.ghostEl.style.left = `${e.clientX - dragged.offsetX}px`;
|
||||
dragged.ghostEl.style.top = `${e.clientY - dragged.offsetY}px`;
|
||||
}
|
||||
});
|
||||
|
||||
gameBoard.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
|
||||
const targetEl = e.target.closest('.card, .pile');
|
||||
let potentialDestPileEl = null;
|
||||
|
||||
if (targetEl) {
|
||||
if (targetEl.classList.contains('card')) {
|
||||
potentialDestPileEl = targetEl.closest('.pile');
|
||||
} else if (targetEl.classList.contains('pile')) {
|
||||
potentialDestPileEl = targetEl;
|
||||
}
|
||||
}
|
||||
|
||||
const currentlyHighlighted = document.querySelector('.pile.drag-over');
|
||||
if (currentlyHighlighted && currentlyHighlighted !== potentialDestPileEl) {
|
||||
currentlyHighlighted.classList.remove('drag-over');
|
||||
}
|
||||
|
||||
if (potentialDestPileEl && dragged.cards.length > 0) {
|
||||
const destPileData = getPileArrayFromElement(potentialDestPileEl);
|
||||
if (canMove(dragged.cards[0], destPileData)) {
|
||||
potentialDestPileEl.classList.add('drag-over');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
gameBoard.addEventListener('dragend', () => {
|
||||
// Clean up ghost element and reset state
|
||||
if (dragged.ghostEl) {
|
||||
dragged.ghostEl.remove();
|
||||
dragged.ghostEl = null;
|
||||
}
|
||||
// Make original cards visible again (updateBoard will re-position them)
|
||||
dragged.cards.forEach(c => cardElements[c.id].style.visibility = 'visible');
|
||||
dragged = { cards: [], sourcePile: null, startIndex: -1, ghostEl: null };
|
||||
document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over'));
|
||||
updateBoard(); // Re-render to ensure everything is correct
|
||||
});
|
||||
|
||||
gameBoard.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over'));
|
||||
|
||||
if (!dragged.cards.length) return;
|
||||
|
||||
const dropTargetEl = e.target.closest('.card, .pile');
|
||||
if (!dropTargetEl) return;
|
||||
const destPile = getPileArrayFromElement(dropTargetEl.closest('.pile'));
|
||||
|
||||
let destPile = null;
|
||||
if (dropTargetEl.classList.contains('card')) {
|
||||
// If dropped on a card, find the pile that card belongs to
|
||||
const cardId = dropTargetEl.dataset.id;
|
||||
const targetCardPile = getPileContainingCard(cardId);
|
||||
if (targetCardPile) {
|
||||
// We need to ensure it's the TOP card of the pile to drop ONTO it effectively
|
||||
if (targetCardPile[targetCardPile.length - 1].id === cardId) {
|
||||
destPile = targetCardPile;
|
||||
}
|
||||
}
|
||||
} else if (dropTargetEl.classList.contains('pile')) {
|
||||
// If dropped directly on a pile (e.g., an empty tableau slot)
|
||||
destPile = getPileArrayFromElement(dropTargetEl);
|
||||
}
|
||||
|
||||
if (destPile && canMove(dragged.cards[0], destPile)) {
|
||||
// --- SCORING LOGIC ---
|
||||
if (foundations.includes(destPile)) score += 10;
|
||||
if (tableau.includes(destPile) && dragged.sourcePile === waste) score += 5;
|
||||
if (tableau.includes(destPile) && foundations.includes(dragged.sourcePile)) score -= 15;
|
||||
// --- END SCORING LOGIC ---
|
||||
pushHistory();
|
||||
moveSequence(dragged.sourcePile, dragged.startIndex, destPile);
|
||||
performMoveCleanup(dragged.sourcePile);
|
||||
}
|
||||
// dragend will handle cleanup regardless of success
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e)=>{
|
||||
|
||||
62
style.css
62
style.css
@ -174,9 +174,10 @@ h1 {
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
transition: top .18s ease, left .18s ease, transform .18s ease, filter .2s ease;
|
||||
transition: top .25s cubic-bezier(0.2, 0.8, 0.2, 1), left .25s cubic-bezier(0.2, 0.8, 0.2, 1), transform .25s cubic-bezier(0.2, 0.8, 0.2, 1), filter .2s ease;
|
||||
will-change: top, left, transform;
|
||||
filter: drop-shadow(0 2px 5px var(--shadow-color));
|
||||
font-size: calc(var(--card-width) * 0.28); /* Scalable base font size */
|
||||
}
|
||||
.card.is-stacked { filter: none; }
|
||||
.card-inner {
|
||||
@ -196,7 +197,7 @@ h1 {
|
||||
.card-face--front {
|
||||
background: linear-gradient(160deg, #ffffff, #f4f4f4);
|
||||
transform: rotateY(180deg);
|
||||
padding: 8px;
|
||||
padding: calc(var(--card-width) * 0.1);
|
||||
cursor: grab;
|
||||
border: 1px solid #d0d0d0;
|
||||
}
|
||||
@ -206,8 +207,8 @@ h1 {
|
||||
|
||||
.card-value-display--top {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
top: calc(var(--card-width) * 0.1);
|
||||
left: calc(var(--card-width) * 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -215,8 +216,8 @@ h1 {
|
||||
}
|
||||
.card-value-display--bottom {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
bottom: calc(var(--card-width) * 0.1);
|
||||
right: calc(var(--card-width) * 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -418,8 +419,29 @@ body.card-back-fade .card .card-face--back,
|
||||
|
||||
/* For Mobile Screens (anything below 600px wide) */
|
||||
@media (max-width: 600px) {
|
||||
.foundations {
|
||||
gap: 5px;
|
||||
:root {
|
||||
/* Dynamically size cards so 7 columns fit perfectly within the 96vw container */
|
||||
--gap: 1.5vw;
|
||||
--card-width: calc((96vw - 20px - (6 * var(--gap))) / 7);
|
||||
}
|
||||
.topbar {
|
||||
padding: 10px 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
.topbar h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.draw-toggle {
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.scores {
|
||||
font-size: 14px;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -435,4 +457,28 @@ body.card-back-fade .card .card-face--back,
|
||||
:root {
|
||||
--card-width: max(7.5vmin, 90px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Add this to your style.css */
|
||||
.card-ghost-stack {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.card-ghost-stack .card {
|
||||
position: absolute !important;
|
||||
box-shadow: 0 12px 30px rgba(0,0,0,0.4);
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* Ensure ghost cards use the current theme's back */
|
||||
.card-ghost-stack .card-face--back {
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.card.dragging {
|
||||
visibility: hidden;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user