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"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Klondike Solitaire</title>
|
<title>Klondike Solitaire</title>
|
||||||
<link rel="stylesheet" href="style.css"/>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
|
|||||||
347
script.js
347
script.js
@ -10,11 +10,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
let deck, stock, waste, foundations, tableau;
|
let deck, stock, waste, foundations, tableau;
|
||||||
let cardElements = {};
|
let cardElements = {};
|
||||||
let dragged = { cards: [], sourcePile: null, startIndex: -1 };
|
let dragged = { cards: [], sourcePile: null, startIndex: -1, ghostEl: null, offsetX: 0, offsetY: 0 };
|
||||||
// 'selected' state is no longer needed for click controls and has been removed.
|
|
||||||
const history = [];
|
const history = [];
|
||||||
|
|
||||||
let isAutoCompleting = false;
|
let isAutoCompleting = false;
|
||||||
|
let autoCompleteInterval = null;
|
||||||
|
let isDealing = false;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let highScore = 0;
|
let highScore = 0;
|
||||||
|
|
||||||
@ -65,6 +66,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
history.push(...gameState.history);
|
history.push(...gameState.history);
|
||||||
DRAW_COUNT = gameState.drawCount || 3;
|
DRAW_COUNT = gameState.drawCount || 3;
|
||||||
score = gameState.score || 0;
|
score = gameState.score || 0;
|
||||||
|
highScore = parseInt(localStorage.getItem('solitaire-high-score'), 10) || 0;
|
||||||
drawSelect.value = DRAW_COUNT;
|
drawSelect.value = DRAW_COUNT;
|
||||||
Object.values(cardElements).forEach(el => el.remove());
|
Object.values(cardElements).forEach(el => el.remove());
|
||||||
cardElements = {};
|
cardElements = {};
|
||||||
@ -102,10 +104,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// ====== SETUP ======
|
// ====== SETUP ======
|
||||||
function initState() {
|
function initState() {
|
||||||
score = 0;
|
score = 0;
|
||||||
highScore = localStorage.getItem('solitaire-high-score') || 0;
|
highScore = parseInt(localStorage.getItem('solitaire-high-score'), 10) || 0;
|
||||||
updateScoreDisplay();
|
updateScoreDisplay();
|
||||||
|
|
||||||
isAutoCompleting = false;
|
isAutoCompleting = false;
|
||||||
|
if (autoCompleteInterval) clearInterval(autoCompleteInterval);
|
||||||
|
|
||||||
Object.values(cardElements).forEach(el => el.remove());
|
Object.values(cardElements).forEach(el => el.remove());
|
||||||
cardElements = {};
|
cardElements = {};
|
||||||
deck = []; stock = []; waste = [];
|
deck = []; stock = []; waste = [];
|
||||||
@ -122,16 +126,54 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const el = createCardElement(cardData);
|
const el = createCardElement(cardData);
|
||||||
cardElements[cardData.id] = el;
|
cardElements[cardData.id] = el;
|
||||||
gameBoard.appendChild(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 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;
|
stock = deck;
|
||||||
|
|
||||||
winMessage.classList.add('hidden');
|
winMessage.classList.add('hidden');
|
||||||
history.length = 0;
|
history.length = 0;
|
||||||
pushHistory();
|
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();
|
saveGame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +190,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
pileData.forEach((cardData, i) => {
|
pileData.forEach((cardData, i) => {
|
||||||
const cardEl = cardElements[cardData.id];
|
const cardEl = cardElements[cardData.id];
|
||||||
if(!cardEl) return;
|
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') {
|
if (type === 'tableau') {
|
||||||
cardEl.style.top = `${baseTop + currentOffset}px`;
|
cardEl.style.top = `${baseTop + currentOffset}px`;
|
||||||
currentOffset += cardData.faceUp ? SPACING_FACE_UP : SPACING_FACE_DOWN;
|
currentOffset += cardData.faceUp ? SPACING_FACE_UP : SPACING_FACE_DOWN;
|
||||||
@ -164,7 +214,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
cardEl.classList.remove('is-flipped');
|
cardEl.classList.remove('is-flipped');
|
||||||
cardEl.draggable = false;
|
cardEl.draggable = false;
|
||||||
}
|
}
|
||||||
// The 'selected' class is no longer applied via click.
|
|
||||||
const zBase = type === 'tableau' ? 100 : type === 'foundation' ? 400 : 600;
|
const zBase = type === 'tableau' ? 100 : type === 'foundation' ? 400 : 600;
|
||||||
cardEl.style.zIndex = zBase + i;
|
cardEl.style.zIndex = zBase + i;
|
||||||
});
|
});
|
||||||
@ -181,6 +230,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
waste.forEach((cardData, i) => {
|
waste.forEach((cardData, i) => {
|
||||||
const cardEl = cardElements[cardData.id];
|
const cardEl = cardElements[cardData.id];
|
||||||
if(!cardEl) return;
|
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 isTopCard = i === waste.length - 1;
|
||||||
const visibleFanIndexStart = Math.max(0, waste.length - DRAW_COUNT);
|
const visibleFanIndexStart = Math.max(0, waste.length - DRAW_COUNT);
|
||||||
let leftOffset = 0;
|
let leftOffset = 0;
|
||||||
@ -266,30 +323,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (type === 'stock') return stock;
|
if (type === 'stock') return stock;
|
||||||
return null;
|
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) {
|
function findCardData(cardEl) {
|
||||||
const id = cardEl.dataset.id;
|
const id = cardEl.dataset.id;
|
||||||
let cardIndex;
|
let cardIndex;
|
||||||
const findIn = (pile) => pile.findIndex(c => c.id === id);
|
|
||||||
if ((cardIndex = findIn(waste)) > -1 && cardIndex === waste.length - 1) {
|
// Check waste (only top card can be moved)
|
||||||
return { sourcePile: waste, cardIndex, card: waste[cardIndex], cardsToMove: [waste[cardIndex]] };
|
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) {
|
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);
|
const cardsToMove = pile.slice(cardIndex);
|
||||||
return { sourcePile: pile, cardIndex, card: cardsToMove[0], cardsToMove };
|
return { sourcePile: pile, cardIndex, card: cardsToMove[0], cardsToMove };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Check foundations (only top card can be moved)
|
||||||
for (const pile of foundations) {
|
for (const pile of foundations) {
|
||||||
if ((cardIndex = findIn(pile)) > -1 && cardIndex === pile.length - 1) {
|
if (pile.length > 0 && pile[pile.length - 1].id === id) {
|
||||||
return { sourcePile: pile, cardIndex, card: pile[cardIndex], cardsToMove: [pile[cardIndex]] };
|
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) {
|
function canMove(card, destPile) {
|
||||||
if (!card || !destPile) return false;
|
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';
|
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) {
|
if (destPile.length === 0) {
|
||||||
return (destType==='tableau' && card.value==='K') || (destType==='foundation' && card.value==='A');
|
return (destType==='tableau' && card.value==='K') || (destType==='foundation' && card.value==='A');
|
||||||
}
|
}
|
||||||
@ -297,13 +385,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (destType === 'tableau') {
|
if (destType === 'tableau') {
|
||||||
const opposite = ((['♥','♦'].includes(card.suit) && ['♣','♠'].includes(top.suit)) || (['♣','♠'].includes(card.suit) && ['♥','♦'].includes(top.suit)));
|
const opposite = ((['♥','♦'].includes(card.suit) && ['♣','♠'].includes(top.suit)) || (['♣','♠'].includes(card.suit) && ['♥','♦'].includes(top.suit)));
|
||||||
return opposite && getValueRank(card.value) === getValueRank(top.value) - 1;
|
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 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) {
|
function moveSequence(sourcePile, startIndex, destPile) {
|
||||||
const seq = sourcePile.splice(startIndex);
|
const seq = sourcePile.splice(startIndex);
|
||||||
destPile.push(...seq);
|
destPile.push(...seq);
|
||||||
@ -330,19 +417,92 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
updateScoreDisplay();
|
updateScoreDisplay();
|
||||||
}
|
}
|
||||||
winMessage.classList.remove('hidden');
|
winMessage.classList.remove('hidden');
|
||||||
try {
|
triggerBouncingCards();
|
||||||
confetti({ particleCount: 180, spread: 75, origin:{y:0.35} });
|
|
||||||
setTimeout(()=>confetti({ particleCount: 140, spread: 65, origin:{y:.2} }), 350);
|
|
||||||
} catch(_) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ======
|
// ====== ACTIONS ======
|
||||||
function handleStockClick() {
|
function handleStockClick() {
|
||||||
if (isAutoCompleting) return;
|
if (isAutoCompleting) return;
|
||||||
pushHistory();
|
pushHistory();
|
||||||
if (stock.length > 0) {
|
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);
|
cardsToMove.forEach(c => c.faceUp = true);
|
||||||
waste.push(...cardsToMove);
|
waste.push(...cardsToMove);
|
||||||
} else if (waste.length > 0) {
|
} else if (waste.length > 0) {
|
||||||
@ -355,10 +515,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
saveGame();
|
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) {
|
function performAutoMove(cardEl) {
|
||||||
if (isAutoCompleting) return;
|
if (isAutoCompleting) return;
|
||||||
const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl);
|
const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl);
|
||||||
@ -374,7 +530,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
pushHistory();
|
pushHistory();
|
||||||
moveSequence(sourcePile, cardIndex, f);
|
moveSequence(sourcePile, cardIndex, f);
|
||||||
performMoveCleanup(sourcePile);
|
performMoveCleanup(sourcePile);
|
||||||
return; // Move executed, action is complete.
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -388,7 +544,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
pushHistory();
|
pushHistory();
|
||||||
moveSequence(sourcePile, cardIndex, t);
|
moveSequence(sourcePile, cardIndex, t);
|
||||||
performMoveCleanup(sourcePile);
|
performMoveCleanup(sourcePile);
|
||||||
return; // Move executed, action is complete.
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -422,7 +578,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isAutoCompleting = true;
|
isAutoCompleting = true;
|
||||||
const intervalId = setInterval(() => {
|
autoCompleteInterval = setInterval(() => {
|
||||||
let movedCard = false;
|
let movedCard = false;
|
||||||
let source = null;
|
let source = null;
|
||||||
|
|
||||||
@ -460,7 +616,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (source) performMoveCleanup(source);
|
if (source) performMoveCleanup(source);
|
||||||
|
|
||||||
if (!movedCard) {
|
if (!movedCard) {
|
||||||
clearInterval(intervalId);
|
clearInterval(autoCompleteInterval);
|
||||||
|
autoCompleteInterval = null;
|
||||||
isAutoCompleting = false;
|
isAutoCompleting = false;
|
||||||
checkWin();
|
checkWin();
|
||||||
}
|
}
|
||||||
@ -479,7 +636,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
for(const f of foundations) if(canMove(topCard, f)) return true;
|
for(const f of foundations) if(canMove(topCard, f)) return true;
|
||||||
|
|
||||||
const movableStackIndex = sourcePile.findIndex(card => card.faceUp);
|
const movableStackIndex = sourcePile.findIndex(card => card.faceUp);
|
||||||
if (movableStackIndex > -1) { // Fixed: check should be > -1
|
if (movableStackIndex > -1) {
|
||||||
const cardToMove = sourcePile[movableStackIndex];
|
const cardToMove = sourcePile[movableStackIndex];
|
||||||
for (const destPile of tableau) {
|
for (const destPile of tableau) {
|
||||||
if (sourcePile !== destPile && canMove(cardToMove, destPile)) return true;
|
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.
|
// Clicks on anything else (background, empty piles, etc.) will do nothing.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Custom drag and drop functionality
|
||||||
gameBoard.addEventListener('dragstart', e => {
|
gameBoard.addEventListener('dragstart', e => {
|
||||||
if (isAutoCompleting) { e.preventDefault(); return; }
|
if (isAutoCompleting) { e.preventDefault(); return; }
|
||||||
const cardEl = e.target.closest('.card');
|
const cardEl = e.target.closest('.card');
|
||||||
if (!cardEl) { e.preventDefault(); return; }
|
if (!cardEl) { e.preventDefault(); return; }
|
||||||
|
|
||||||
const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl);
|
const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl);
|
||||||
if (!sourcePile || !cardsToMove.length) { e.preventDefault(); return; }
|
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 => {
|
gameBoard.addEventListener('drop', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over'));
|
||||||
|
|
||||||
if (!dragged.cards.length) return;
|
if (!dragged.cards.length) return;
|
||||||
|
|
||||||
const dropTargetEl = e.target.closest('.card, .pile');
|
const dropTargetEl = e.target.closest('.card, .pile');
|
||||||
if (!dropTargetEl) return;
|
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)) {
|
if (destPile && canMove(dragged.cards[0], destPile)) {
|
||||||
|
// --- SCORING LOGIC ---
|
||||||
if (foundations.includes(destPile)) score += 10;
|
if (foundations.includes(destPile)) score += 10;
|
||||||
if (tableau.includes(destPile) && dragged.sourcePile === waste) score += 5;
|
if (tableau.includes(destPile) && dragged.sourcePile === waste) score += 5;
|
||||||
if (tableau.includes(destPile) && foundations.includes(dragged.sourcePile)) score -= 15;
|
if (tableau.includes(destPile) && foundations.includes(dragged.sourcePile)) score -= 15;
|
||||||
|
// --- END SCORING LOGIC ---
|
||||||
pushHistory();
|
pushHistory();
|
||||||
moveSequence(dragged.sourcePile, dragged.startIndex, destPile);
|
moveSequence(dragged.sourcePile, dragged.startIndex, destPile);
|
||||||
performMoveCleanup(dragged.sourcePile);
|
performMoveCleanup(dragged.sourcePile);
|
||||||
}
|
}
|
||||||
|
// dragend will handle cleanup regardless of success
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (e)=>{
|
document.addEventListener('keydown', (e)=>{
|
||||||
|
|||||||
62
style.css
62
style.css
@ -174,9 +174,10 @@ h1 {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
touch-action: 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;
|
will-change: top, left, transform;
|
||||||
filter: drop-shadow(0 2px 5px var(--shadow-color));
|
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.is-stacked { filter: none; }
|
||||||
.card-inner {
|
.card-inner {
|
||||||
@ -196,7 +197,7 @@ h1 {
|
|||||||
.card-face--front {
|
.card-face--front {
|
||||||
background: linear-gradient(160deg, #ffffff, #f4f4f4);
|
background: linear-gradient(160deg, #ffffff, #f4f4f4);
|
||||||
transform: rotateY(180deg);
|
transform: rotateY(180deg);
|
||||||
padding: 8px;
|
padding: calc(var(--card-width) * 0.1);
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
border: 1px solid #d0d0d0;
|
border: 1px solid #d0d0d0;
|
||||||
}
|
}
|
||||||
@ -206,8 +207,8 @@ h1 {
|
|||||||
|
|
||||||
.card-value-display--top {
|
.card-value-display--top {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: calc(var(--card-width) * 0.1);
|
||||||
left: 8px;
|
left: calc(var(--card-width) * 0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -215,8 +216,8 @@ h1 {
|
|||||||
}
|
}
|
||||||
.card-value-display--bottom {
|
.card-value-display--bottom {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 8px;
|
bottom: calc(var(--card-width) * 0.1);
|
||||||
right: 8px;
|
right: calc(var(--card-width) * 0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -418,8 +419,29 @@ body.card-back-fade .card .card-face--back,
|
|||||||
|
|
||||||
/* For Mobile Screens (anything below 600px wide) */
|
/* For Mobile Screens (anything below 600px wide) */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.foundations {
|
:root {
|
||||||
gap: 5px;
|
/* 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,3 +458,27 @@ body.card-back-fade .card .card-face--back,
|
|||||||
--card-width: max(7.5vmin, 90px);
|
--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