some fun
This commit is contained in:
+166
-8
@@ -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 для разных цветов
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user