This commit is contained in:
jahlib
2025-11-10 12:45:02 +03:00
committed by GitHub
parent a793aa57a3
commit 3545c91bfd
4 changed files with 314 additions and 10 deletions
+166 -8
View File
@@ -7,6 +7,7 @@ class CardGame {
this.hand = [];
this.pendingCardToPlay = null;
this.eightDrawnCards = []; // ID карт взятых из колоды на восьмёрку
this.lastShakeTime = 0; // Время последней тряски для кулдауна
// Загружаем состояние звука из localStorage
this.soundEnabled = localStorage.getItem('soundEnabled') !== 'false';
@@ -78,14 +79,24 @@ class CardGame {
}
playSound(soundName) {
// Не воспроизводим звуки если страница скрыта
if (this.soundEnabled && this.sounds[soundName] && this.pageVisible) {
// Используем существующий объект вместо клонирования
const sound = this.sounds[soundName];
sound.currentTime = 0; // Сбрасываем на начало для повторного воспроизведения
sound.volume = 0.5;
sound.play().catch(err => {}); // Убираем console.log для производительности
if (!this.soundEnabled || !this.sounds[soundName]) {
return;
}
// Проверяем на каком экране мы находимся
const isOnLobby = this.lobbyScreen && this.lobbyScreen.classList.contains('active');
// Если на главной странице и вкладка скрыта - не воспроизводим звук
if (isOnLobby && !this.pageVisible) {
return;
}
// Во время игры (room или game экран) звуки работают даже в фоне
// Используем существующий объект вместо клонирования
const sound = this.sounds[soundName];
sound.currentTime = 0; // Сбрасываем на начало для повторного воспроизведения
sound.volume = 0.5;
sound.play().catch(err => {}); // Убираем console.log для производительности
}
toggleSound() {
@@ -282,6 +293,10 @@ class CardGame {
this.sendChatBtn = document.getElementById('send-chat-btn');
this.closeChatBtn = document.getElementById('close-chat-btn');
// Reaction elements
this.reactionPicker = document.getElementById('reaction-picker');
this.playerInfo = document.querySelector('.player-info');
// Rules elements
this.rulesBtn = document.getElementById('rules-btn');
this.rulesModal = document.getElementById('rules-modal');
@@ -411,6 +426,33 @@ class CardGame {
}
});
// Reaction handlers - клик по блоку с никнеймом
if (this.playerInfo) {
this.playerInfo.addEventListener('click', (e) => {
e.stopPropagation();
this.showReactionPicker(e);
});
}
document.querySelectorAll('.reaction-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const reaction = e.currentTarget.dataset.reaction;
this.sendReaction(reaction);
this.hideReactionPicker();
});
});
// Закрываем пикер при клике вне его
document.addEventListener('click', (e) => {
if (this.reactionPicker &&
this.reactionPicker.classList.contains('active') &&
!this.reactionPicker.contains(e.target) &&
(!this.playerInfo || !this.playerInfo.contains(e.target))) {
this.hideReactionPicker();
}
});
// Rules handlers
this.rulesBtn.addEventListener('click', () => this.openRules());
this.closeRulesBtn.addEventListener('click', () => this.closeRules());
@@ -847,6 +889,16 @@ class CardGame {
// Игрок переподключился
this.addLogEntry(`${data.nickname} переподключился`);
break;
case 'shake_discard':
// Тряска карты сброса
this.animateShakeDiscard();
this.playSound('alert');
break;
case 'reaction':
// Быстрая реакция от игрока
this.playSound('chat');
this.showReactionBubble(data.player_id, data.emoji);
break;
case 'deck_size_changed':
// Размер колоды изменён
if (this.currentRoom) {
@@ -1136,6 +1188,8 @@ class CardGame {
this.discardPile.innerHTML = '';
if (data.top_card) {
const cardElement = this.createCardElement(data.top_card, false);
cardElement.style.cursor = 'pointer';
cardElement.addEventListener('click', () => this.shakeDiscardPile());
this.discardPile.appendChild(cardElement);
}
@@ -1462,7 +1516,28 @@ class CardGame {
return;
}
this.send({ type: 'skip_turn' });
this.send({
type: 'skip_turn'
});
}
shakeDiscardPile() {
// Проверяем кулдаун (5 секунд)
const now = Date.now();
const cooldown = 5000; // 5 секунд в миллисекундах
if (now - this.lastShakeTime < cooldown) {
// Просто игнорируем клик если кулдаун активен
return;
}
// Обновляем время последней тряски
this.lastShakeTime = now;
// Отправляем событие тряски на сервер
this.send({
type: 'shake_discard'
});
}
showCountdown(seconds) {
@@ -1691,6 +1766,76 @@ class CardGame {
this.closeChat();
}
showReactionPicker(e) {
e.stopPropagation();
if (!this.reactionPicker || !this.handCards) return;
// Получаем координаты руки карт
const rect = this.handCards.getBoundingClientRect();
// Позиционируем пикер НАД рукой карт по центру
// Ширина пикера: 5 кнопок * 50px + отступы (8px * 6) = 298px
const vh = window.innerHeight / 100;
this.reactionPicker.style.left = `${rect.left + rect.width / 2 - 149}px`; // Центрируем
this.reactionPicker.style.top = `${rect.top - 70 - vh}px`; // Над рукой + 1vh выше
this.reactionPicker.classList.add('active');
}
hideReactionPicker() {
if (this.reactionPicker) {
this.reactionPicker.classList.remove('active');
}
}
sendReaction(emoji) {
this.send({
type: 'reaction',
emoji: emoji
});
}
showReactionBubble(playerId, emoji) {
// Находим область игрока
let playerArea;
if (playerId === this.playerId) {
// Реакция от нас самих - показываем над нашими картами
playerArea = this.handCards;
} else {
// Реакция от противника
playerArea = this.getOpponentAreaById(playerId);
}
if (!playerArea) return;
// Создаём пузырёк
const bubble = document.createElement('div');
bubble.className = 'reaction-bubble';
bubble.textContent = emoji;
// Позиционируем пузырёк и добавляем стрелочку
const rect = playerArea.getBoundingClientRect();
bubble.style.left = `${rect.left + rect.width / 2 - 40}px`; // Центрируем
if (playerId === this.playerId) {
// Наш пузырёк - НАД картами, стрелка ВНИЗ на наши карты
bubble.style.top = `${rect.top - 70}px`;
bubble.classList.add('from-me');
} else {
// Пузырёк противника - ПОД картами, стрелка ВВЕРХ на карты противника
bubble.style.top = `${rect.bottom + 10}px`;
bubble.classList.add('from-opponent');
}
document.body.appendChild(bubble);
// Удаляем пузырёк после анимации
setTimeout(() => {
bubble.remove();
}, 2000);
}
handleKeyPress(e) {
// Проверяем что мы не в поле ввода (кроме чата)
const isInInput = e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA';
@@ -1944,6 +2089,19 @@ class CardGame {
}, cardCount * 80);
}
// Анимация тряски карты сброса
animateShakeDiscard() {
if (!this.discardPile) return;
// Добавляем класс для анимации тряски
this.discardPile.classList.add('shaking');
// Убираем класс после завершения анимации
setTimeout(() => {
this.discardPile.classList.remove('shaking');
}, 500);
}
getPlayerColorIndex(playerId) {
// Создаем простой хеш из ID игрока для определения цвета
// Возвращаем число от 1 до 3 для разных цветов
+9
View File
@@ -245,6 +245,15 @@
</div>
</div>
<!-- Модальное окно быстрых реакций -->
<div id="reaction-picker" class="reaction-picker">
<button class="reaction-btn" data-reaction="😡">😡</button>
<button class="reaction-btn" data-reaction="😄">😄</button>
<button class="reaction-btn" data-reaction="😎">😎</button>
<button class="reaction-btn" data-reaction="🤯">🤯</button>
<button class="reaction-btn" data-reaction="🙃">🙃</button>
</div>
<!-- Модальное окно выбора масти -->
<div id="suit-modal" class="modal">
<div class="modal-content">
+10 -2
View File
@@ -1,10 +1,18 @@
/* ===== ПОЛНЫЙ НОЧНОЙ РЕЖИМ ===== */
/* Плавные переходы для всех элементов кроме летящих карт */
*:not(.flying-card):not(.flying-card *) {
/* Плавные переходы для всех элементов кроме летящих карт и тряски */
*:not(.flying-card):not(.flying-card *):not(.shaking):not(.shaking *) {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, filter 0.3s ease, transform 0.3s ease, opacity 0.3s ease, width 0.3s ease, height 0.3s ease, margin 0.3s ease, padding 0.3s ease;
}
/* Отключаем transition для летящих карт и тряски */
.flying-card,
.flying-card *,
.shaking,
.shaking * {
transition: none !important;
}
/* Базовые стили */
body.night-mode {
background: #1a2332;
+129
View File
@@ -1229,6 +1229,13 @@ border: 2px solid #ffd700;
border-bottom: none;
margin-left: 0px;
align-self: flex-start;
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease;
}
.player-info:hover {
background: rgba(255, 255, 255, 0.9);
}
.player-hand {
@@ -2192,3 +2199,125 @@ border: 2px solid #ffd700;
transform: scale(1.1) rotate(-3deg);
}
}
/* Discard pile shake animation */
.shaking {
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% {
transform: translateX(0) rotate(0deg);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-10px) rotate(-3deg);
}
20%, 40%, 60%, 80% {
transform: translateX(10px) rotate(3deg);
}
}
/* Reaction picker */
.reaction-picker {
display: none;
position: fixed;
background: rgba(30, 30, 30, 0.95);
border-radius: 12px;
padding: 8px;
gap: 8px;
z-index: 10000;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
animation: popIn 0.2s ease-out;
}
.reaction-picker.active {
display: flex;
}
.reaction-btn {
width: 50px;
height: 50px;
border: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
font-size: 28px;
cursor: pointer;
transition: all 0.2s ease;
}
.reaction-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.15);
}
.reaction-btn:active {
transform: scale(0.95);
}
@keyframes popIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Reaction bubble */
.reaction-bubble {
position: absolute;
background: rgba(30, 30, 30, 0.95);
color: white;
padding: 12px 16px;
border-radius: 20px;
font-size: 32px;
z-index: 9999;
pointer-events: none;
animation: reactionFloat 2s ease-out forwards;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
}
/* Стрелочка пузырька */
.reaction-bubble::after {
content: '';
position: absolute;
width: 0;
height: 0;
border-style: solid;
left: 50%;
transform: translateX(-50%);
}
/* Стрелочка вниз (от меня - пузырёк над моими картами) */
.reaction-bubble.from-me::after {
bottom: -12px;
border-width: 12px 12px 0 12px;
border-color: rgba(30, 30, 30, 0.95) transparent transparent transparent;
}
/* Стрелочка вверх (от противника - пузырёк под его картами) */
.reaction-bubble.from-opponent::after {
top: -12px;
border-width: 0 12px 12px 12px;
border-color: transparent transparent rgba(30, 30, 30, 0.95) transparent;
}
@keyframes reactionFloat {
0% {
opacity: 0;
transform: translateY(0) scale(0.5);
}
20% {
opacity: 1;
transform: translateY(-10px) scale(1.1);
}
40% {
transform: translateY(-5px) scale(1);
}
100% {
opacity: 0;
transform: translateY(-30px) scale(0.8);
}
}