// /public/js/uno.js
document.addEventListener('DOMContentLoaded', () => {
const configContainer = document.getElementById('game-config');
if (!configContainer) return;
const config = {
gameId: configContainer.dataset.gameId,
myId: parseInt(configContainer.dataset.myId),
myRole: parseInt(configContainer.dataset.myRole)
};
const els = {
oppHand: document.getElementById('opp-hand'),
discardPile: document.getElementById('discard-pile'),
myHand: document.getElementById('my-hand'),
deck: document.getElementById('draw-deck'),
colorModal: document.getElementById('color-modal'),
oppName: document.getElementById('opp-name'),
oppCardCount: document.getElementById('opp-card-count'),
myCardCount: document.getElementById('my-card-count'),
colorDot: document.getElementById('color-dot'),
statusToast: document.getElementById('status-toast'),
myPanel: document.getElementById('my-panel'),
oppPanel: document.getElementById('opponent-panel')
};
let gameState = {
isMyTurn: false,
myHand: [],
pendingCardIdx: null,
pendingCardElement: null
};
const animations = {
duration: 800,
createFlyingCard(cardHTML, fromRect, toRect) {
const flyingCard = document.createElement('div');
flyingCard.className = 'flying-card';
flyingCard.innerHTML = cardHTML;
flyingCard.style.position = 'fixed';
flyingCard.style.left = fromRect.left + 'px';
flyingCard.style.top = fromRect.top + 'px';
flyingCard.style.width = fromRect.width + 'px';
flyingCard.style.height = fromRect.height + 'px';
flyingCard.style.zIndex = '9999';
flyingCard.style.pointerEvents = 'none';
document.body.appendChild(flyingCard);
const deltaX = toRect.left - fromRect.left;
const deltaY = toRect.top - fromRect.top;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const midX = fromRect.left + deltaX / 2;
const midY = fromRect.top + deltaY / 2 - distance * 0.3;
const path = `path("M ${fromRect.left + fromRect.width/2},${fromRect.top + fromRect.height/2} Q ${midX},${midY} ${toRect.left + toRect.width/2},${toRect.top + toRect.height/2}")`;
requestAnimationFrame(() => {
flyingCard.style.transition = `all ${this.duration}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
flyingCard.style.left = toRect.left + 'px';
flyingCard.style.top = toRect.top + 'px';
flyingCard.style.width = toRect.width + 'px';
flyingCard.style.height = toRect.height + 'px';
flyingCard.style.transform = 'rotate(360deg)';
});
return flyingCard;
},
animateDraw(callback) {
const deckRect = els.deck.getBoundingClientRect();
const handRect = els.myHand.getBoundingClientRect();
const targetRect = {
left: handRect.left + handRect.width / 2 - 45,
top: handRect.top + handRect.height / 2 - 67.5,
width: 90,
height: 135
};
const cardHTML = '
';
const flyingCard = this.createFlyingCard(cardHTML, deckRect, targetRect);
setTimeout(() => {
flyingCard.remove();
if (callback) callback();
}, this.duration);
},
animatePlay(cardElement, callback) {
const cardRect = cardElement.getBoundingClientRect();
const discardRect = els.discardPile.getBoundingClientRect();
const targetRect = {
left: discardRect.left + discardRect.width / 2 - cardRect.width / 2,
top: discardRect.top + discardRect.height / 2 - cardRect.height / 2,
width: cardRect.width,
height: cardRect.height
};
const cardHTML = cardElement.outerHTML;
const flyingCard = this.createFlyingCard(cardHTML, cardRect, targetRect);
cardElement.style.opacity = '0';
setTimeout(() => {
flyingCard.remove();
if (callback) callback();
}, this.duration);
}
};
els.deck.addEventListener('click', () => {
if (!gameState.isMyTurn) {
showToast('Not your turn!');
return;
}
gameState.isMyTurn = false;
updateTurnIndicators();
els.deck.style.transform = 'scale(0.95)';
setTimeout(() => els.deck.style.transform = '', 100);
animations.animateDraw(() => {
fetch('/uno/draw_card', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `id=${config.gameId}`
})
.then(() => syncGame());
});
});
window.playCard = function(index) {
if (!gameState.isMyTurn) {
showToast('Not your turn!');
return;
}
const card = gameState.myHand[index];
const cardElement = els.myHand.children[index];
if (card.startsWith('wild')) {
gameState.pendingCardIdx = index;
gameState.pendingCardElement = cardElement;
showColorPicker();
} else {
sendMove(index, null, cardElement);
}
};
function showColorPicker() {
els.colorModal.style.display = 'block';
}
window.pickColor = function(color) {
els.colorModal.style.display = 'none';
if (gameState.pendingCardIdx !== null) {
sendMove(gameState.pendingCardIdx, color, gameState.pendingCardElement);
gameState.pendingCardIdx = null;
gameState.pendingCardElement = null;
}
};
function sendMove(idx, color, cardElement) {
gameState.isMyTurn = false;
updateTurnIndicators();
animations.animatePlay(cardElement, () => {
const body = `id=${config.gameId}&idx=${idx}${color ? `&color=${color}` : ''}`;
fetch('/uno/play_card', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body
})
.then(res => res.json())
.then(data => {
if (data.success) {
syncGame();
} else {
showToast('Invalid move!');
syncGame();
}
});
});
}
function renderCard(cardString, isPlayable = false, index = -1) {
let [color, val] = cardString.split('_');
if (cardString.startsWith('wild')) {
color = 'wild';
val = cardString === 'wild' ? 'W' : '+4';
}
if (val === 'skip') val = 'ð«';
if (val === 'reverse') val = 'ð';
if (val === 'draw2') val = '+2';
const div = document.createElement('div');
div.className = `uno-card uno-${color}`;
if (isPlayable) {
div.onclick = () => window.playCard(index);
} else if (index !== -1) {
div.classList.add('uno-disabled');
}
div.innerHTML = `${val}
`;
return div;
}
function syncGame() {
fetch(`/uno/play/${config.gameId}`, {
method: 'GET',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(res => res.json())
.then(data => {
gameState.myHand = data.myhand;
gameState.isMyTurn = (data.turn === config.myId && data.status === 'active');
updateTurnIndicators();
if (data.status === 'finished') {
if (data.winner === config.myId) {
showToast('ð You Win! ð', 3000);
} else {
showToast('ð You Lose', 3000);
}
return;
}
els.oppHand.innerHTML = '';
for (let i = 0; i < data.oppcount; i++) {
const back = document.createElement('div');
back.className = 'card-back';
els.oppHand.appendChild(back);
}
const topCard = data.topcard;
els.discardPile.innerHTML = '';
if (topCard) {
const discardCard = renderCard(topCard);
els.discardPile.appendChild(discardCard);
}
els.myHand.innerHTML = '';
gameState.myHand.forEach((card, idx) => {
const isPlayable = canPlayCard(card, topCard, data.color);
const cardEl = renderCard(card, isPlayable && gameState.isMyTurn, idx);
els.myHand.appendChild(cardEl);
});
const colorMap = { red: '#ef4444', blue: '#3b82f6', green: '#22c55e', yellow: '#eab308' };
els.colorDot.style.backgroundColor = colorMap[data.color] || '#8b5cf6';
if (els.myCardCount) els.myCardCount.textContent = `${gameState.myHand.length} cards`;
if (els.oppCardCount) els.oppCardCount.textContent = `${data.oppcount} cards`;
});
}
function canPlayCard(card, topCard, currentColor) {
if (card.startsWith('wild')) return true;
const [cardColor, cardVal] = card.split('_');
const [topColor, topVal] = topCard.split('_');
if (cardColor === currentColor) return true;
if (cardVal && topVal && cardVal === topVal) return true;
return false;
}
function updateTurnIndicators() {
if (gameState.isMyTurn) {
els.myPanel.classList.add('active');
els.oppPanel.classList.remove('active');
if (els.deck.parentElement) els.deck.parentElement.classList.add('my-turn');
} else {
els.myPanel.classList.remove('active');
els.oppPanel.classList.add('active');
if (els.deck.parentElement) els.deck.parentElement.classList.remove('my-turn');
}
}
function showToast(message, duration = 1500) {
els.statusToast.textContent = message;
els.statusToast.classList.add('show');
setTimeout(() => {
els.statusToast.classList.remove('show');
}, duration);
}
syncGame();
setInterval(syncGame, 2000);
});