add sound
This commit is contained in:
@@ -7,10 +7,63 @@ class CardGame {
|
||||
this.hand = [];
|
||||
this.pendingCardToPlay = null;
|
||||
this.eightDrawnCards = []; // ID карт взятых из колоды на восьмёрку
|
||||
this.lastShakeTime = 0; // Время последней тряски для кулдауна
|
||||
this.lastReactionTime = 0; // Время последней реакции для кулдауна
|
||||
|
||||
// Загружаем состояние звука из localStorage
|
||||
this.soundEnabled = localStorage.getItem('soundEnabled') !== 'false';
|
||||
|
||||
// Маппинг звуков для реакций
|
||||
this.reactionSounds = {
|
||||
'🙃': 'chat',
|
||||
'🙁': 'chat',
|
||||
'😡': 'error',
|
||||
'😈': 'rizz',
|
||||
'😇': 'chat',
|
||||
'😠': 'error',
|
||||
'😄': 'haha',
|
||||
'😎': 'sigma',
|
||||
'🤔': 'pogodika',
|
||||
'😤': 'ufff',
|
||||
'😐': 'chat',
|
||||
'👍': 'thum',
|
||||
'👎': 'thum',
|
||||
'🤝': 'chat',
|
||||
'🫰': 'chat',
|
||||
'🪬': 'mind',
|
||||
'🤯': 'chat',
|
||||
'🤡': 'clown',
|
||||
'🤨': 'vine',
|
||||
'😑': 'chat',
|
||||
'😌': 'chat',
|
||||
'😴': 'deny',
|
||||
'🌚': 'cave',
|
||||
'🐱': 'cat',
|
||||
'🐸': 'frog',
|
||||
'🐺': 'driff',
|
||||
'🐊': 'dance',
|
||||
'🐴': 'horse',
|
||||
'🐠': 'fish',
|
||||
'🦆': 'duck',
|
||||
'🌹': 'wow',
|
||||
'🗿': 'huh',
|
||||
'👁️': 'eye',
|
||||
'💩': 'fart',
|
||||
'🔩': 'pipe',
|
||||
'🔪': 'okay',
|
||||
'⚔️': 'sword',
|
||||
'🧀': 'meme',
|
||||
'🎺': 'trumpet',
|
||||
'🎁': 'chat',
|
||||
'🔮': 'magic',
|
||||
'🎲': 'dice',
|
||||
'🎯': 'chat',
|
||||
'♥️': 'chat',
|
||||
'♦️': 'chat',
|
||||
'♣️': 'chat',
|
||||
'♠️': 'chat'
|
||||
};
|
||||
|
||||
// Инициализация звуков
|
||||
this.sounds = {
|
||||
playcard: new Audio('/sounds/playcard.aac'),
|
||||
@@ -20,6 +73,7 @@ class CardGame {
|
||||
skip: new Audio('/sounds/skip.aac'),
|
||||
alert: new Audio('/sounds/alert.aac'),
|
||||
chat: new Audio('/sounds/chat.aac'),
|
||||
frog: new Audio('/sounds/frog.aac'),
|
||||
win: new Audio('/sounds/win.aac'),
|
||||
winqueen: new Audio('/sounds/winqueen.aac'),
|
||||
lose: new Audio('/sounds/lose.aac'),
|
||||
@@ -28,7 +82,36 @@ class CardGame {
|
||||
seven: new Audio('/sounds/seven.aac'),
|
||||
shuffle: new Audio('/sounds/shuffle.aac'),
|
||||
ace: new Audio('/sounds/ace.aac'),
|
||||
eightplace: new Audio('/sounds/eightplace.aac')
|
||||
eightplace: new Audio('/sounds/eightplace.aac'),
|
||||
cat: new Audio('/sounds/cat.aac'),
|
||||
okay: new Audio('/sounds/okay.aac'),
|
||||
sword: new Audio('/sounds/sword.aac'),
|
||||
pipe: new Audio('/sounds/pipe.aac'),
|
||||
huh: new Audio('/sounds/huh.aac'),
|
||||
pogodika: new Audio('/sounds/pogodika.aac'),
|
||||
dance: new Audio('/sounds/dance.aac'),
|
||||
fart: new Audio('/sounds/fart.aac'),
|
||||
wow: new Audio('/sounds/wow.aac'),
|
||||
cave: new Audio('/sounds/cave.aac'),
|
||||
trumpet: new Audio('/sounds/trumpet.aac'),
|
||||
driff: new Audio('/sounds/driff.aac'),
|
||||
meme: new Audio('/sounds/meme.aac'),
|
||||
error: new Audio('/sounds/error.aac'),
|
||||
vine: new Audio('/sounds/vine.aac'),
|
||||
duck: new Audio('/sounds/duck.aac'),
|
||||
thum: new Audio('/sounds/thum.aac'),
|
||||
eye: new Audio('/sounds/eye.aac'),
|
||||
sigma: new Audio('/sounds/sigma.aac'),
|
||||
magic: new Audio('/sounds/magic.aac'),
|
||||
rizz: new Audio('/sounds/rizz.aac'),
|
||||
mind: new Audio('/sounds/mind.aac'),
|
||||
haha: new Audio('/sounds/haha.aac'),
|
||||
ufff: new Audio('/sounds/ufff.aac'),
|
||||
clown: new Audio('/sounds/clown.aac'),
|
||||
deny: new Audio('/sounds/deny.aac'),
|
||||
dice: new Audio('/sounds/dice.aac'),
|
||||
fish: new Audio('/sounds/fish.aac'),
|
||||
horse: new Audio('/sounds/horse.aac')
|
||||
};
|
||||
|
||||
this.initElements();
|
||||
@@ -78,14 +161,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() {
|
||||
@@ -97,6 +190,145 @@ class CardGame {
|
||||
this.animationsEnabled = this.animationsToggle.checked;
|
||||
localStorage.setItem('animationsEnabled', this.animationsEnabled);
|
||||
}
|
||||
|
||||
toggleProMode() {
|
||||
this.proModeEnabled = this.proModeToggle.checked;
|
||||
localStorage.setItem('proModeEnabled', this.proModeEnabled);
|
||||
|
||||
// Обновляем отображение карт в руке если игра идёт
|
||||
if (this.hand && this.hand.length > 0 && this.topCard) {
|
||||
this.updateHand(this.topCard, this.chosenSuit);
|
||||
}
|
||||
}
|
||||
|
||||
toggleNightMode() {
|
||||
this.nightModeEnabled = this.nightModeToggle.checked;
|
||||
localStorage.setItem('nightModeEnabled', this.nightModeEnabled);
|
||||
|
||||
if (this.nightModeEnabled) {
|
||||
document.body.classList.add('night-mode');
|
||||
// Показываем настройку белых мастей
|
||||
if (this.whiteSuitsSetting) {
|
||||
this.whiteSuitsSetting.style.display = 'flex';
|
||||
}
|
||||
// Восстанавливаем состояние белых мастей если оно было сохранено
|
||||
const savedWhiteSuits = localStorage.getItem('whiteSuitsEnabled');
|
||||
if (savedWhiteSuits === 'true') {
|
||||
this.whiteSuitsEnabled = true;
|
||||
if (this.whiteSuitsToggle) {
|
||||
this.whiteSuitsToggle.checked = true;
|
||||
}
|
||||
document.body.classList.add('white-suits');
|
||||
}
|
||||
} else {
|
||||
document.body.classList.remove('night-mode');
|
||||
// Скрываем настройку белых мастей
|
||||
if (this.whiteSuitsSetting) {
|
||||
this.whiteSuitsSetting.style.display = 'none';
|
||||
}
|
||||
// Выключаем белые масти
|
||||
this.whiteSuitsEnabled = false;
|
||||
document.body.classList.remove('white-suits');
|
||||
}
|
||||
|
||||
this.updateNightModeButton();
|
||||
}
|
||||
|
||||
toggleNightModeFromButton() {
|
||||
this.nightModeEnabled = !this.nightModeEnabled;
|
||||
localStorage.setItem('nightModeEnabled', this.nightModeEnabled);
|
||||
|
||||
if (this.nightModeEnabled) {
|
||||
document.body.classList.add('night-mode');
|
||||
// Показываем настройку белых мастей
|
||||
if (this.whiteSuitsSetting) {
|
||||
this.whiteSuitsSetting.style.display = 'flex';
|
||||
}
|
||||
// Восстанавливаем состояние белых мастей если оно было сохранено
|
||||
const savedWhiteSuits = localStorage.getItem('whiteSuitsEnabled');
|
||||
if (savedWhiteSuits === 'true') {
|
||||
this.whiteSuitsEnabled = true;
|
||||
if (this.whiteSuitsToggle) {
|
||||
this.whiteSuitsToggle.checked = true;
|
||||
}
|
||||
document.body.classList.add('white-suits');
|
||||
}
|
||||
} else {
|
||||
document.body.classList.remove('night-mode');
|
||||
// Скрываем настройку белых мастей
|
||||
if (this.whiteSuitsSetting) {
|
||||
this.whiteSuitsSetting.style.display = 'none';
|
||||
}
|
||||
// Выключаем белые масти
|
||||
this.whiteSuitsEnabled = false;
|
||||
document.body.classList.remove('white-suits');
|
||||
}
|
||||
|
||||
// Синхронизируем с переключателем в настройках
|
||||
if (this.nightModeToggle) {
|
||||
this.nightModeToggle.checked = this.nightModeEnabled;
|
||||
}
|
||||
|
||||
this.updateNightModeButton();
|
||||
}
|
||||
|
||||
updateNightModeButton() {
|
||||
if (!this.nightModeToggleBtn) return;
|
||||
|
||||
const sunIcon = this.nightModeToggleBtn.querySelector('.sun-icon');
|
||||
const moonIcon = this.nightModeToggleBtn.querySelector('.moon-icon');
|
||||
|
||||
if (this.nightModeEnabled) {
|
||||
// Ночной режим включен - показываем солнце (переключит на дневной)
|
||||
if (sunIcon) sunIcon.style.display = 'inline';
|
||||
if (moonIcon) moonIcon.style.display = 'none';
|
||||
} else {
|
||||
// Дневной режим - показываем луну (переключит на ночной)
|
||||
if (sunIcon) sunIcon.style.display = 'none';
|
||||
if (moonIcon) moonIcon.style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
toggleWhiteSuits() {
|
||||
this.whiteSuitsEnabled = this.whiteSuitsToggle.checked;
|
||||
localStorage.setItem('whiteSuitsEnabled', this.whiteSuitsEnabled);
|
||||
|
||||
if (this.whiteSuitsEnabled) {
|
||||
document.body.classList.add('white-suits');
|
||||
} else {
|
||||
document.body.classList.remove('white-suits');
|
||||
}
|
||||
}
|
||||
|
||||
setCardBackColor(color) {
|
||||
this.cardBackColor = color;
|
||||
localStorage.setItem('cardBackColor', color);
|
||||
document.body.setAttribute('data-card-back', color);
|
||||
|
||||
// Обновляем выделение в селекторе
|
||||
document.querySelectorAll('.card-back-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
});
|
||||
const selectedOption = document.querySelector(`.card-back-option[data-back="${color}"]`);
|
||||
if (selectedOption) {
|
||||
selectedOption.classList.add('selected');
|
||||
}
|
||||
}
|
||||
|
||||
setBackgroundColor(color) {
|
||||
this.backgroundColor = color;
|
||||
localStorage.setItem('backgroundColor', color);
|
||||
document.body.setAttribute('data-background', color);
|
||||
|
||||
// Обновляем выделение в селекторе
|
||||
document.querySelectorAll('.background-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
});
|
||||
const selectedOption = document.querySelector(`.background-option[data-bg="${color}"]`);
|
||||
if (selectedOption) {
|
||||
selectedOption.classList.add('selected');
|
||||
}
|
||||
}
|
||||
|
||||
showAlert(message) {
|
||||
if (!this.alertModal || !this.alertText) return;
|
||||
@@ -154,8 +386,18 @@ class CardGame {
|
||||
|
||||
goToLobby() {
|
||||
this.clearLocalStorage();
|
||||
|
||||
// Закрываем WebSocket соединение перед перезагрузкой
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
window.history.pushState({}, '', '/');
|
||||
location.reload();
|
||||
|
||||
// Небольшая задержка чтобы сообщение успело отправиться
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 40);
|
||||
}
|
||||
|
||||
initElements() {
|
||||
@@ -175,6 +417,7 @@ class CardGame {
|
||||
this.inviteLink = document.getElementById('invite-link');
|
||||
this.copyLinkBtn = document.getElementById('copy-link-btn');
|
||||
this.shareLinkBtn = document.getElementById('share-link-btn');
|
||||
this.nightModeToggleBtn = document.getElementById('night-mode-toggle-btn');
|
||||
|
||||
// Room elements
|
||||
this.playersList = document.getElementById('players-list');
|
||||
@@ -223,6 +466,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');
|
||||
@@ -237,6 +484,10 @@ class CardGame {
|
||||
this.fullscreenToggle = document.getElementById('fullscreen-toggle');
|
||||
this.soundToggle = document.getElementById('sound-toggle');
|
||||
this.animationsToggle = document.getElementById('animations-toggle');
|
||||
this.proModeToggle = document.getElementById('pro-mode-toggle');
|
||||
this.nightModeToggle = document.getElementById('night-mode-toggle');
|
||||
this.whiteSuitsToggle = document.getElementById('white-suits-toggle');
|
||||
this.whiteSuitsSetting = document.getElementById('white-suits-setting');
|
||||
this.logToggle = document.getElementById('log-toggle');
|
||||
|
||||
// Leave game button and modal
|
||||
@@ -253,6 +504,43 @@ class CardGame {
|
||||
// Инициализируем состояние анимаций
|
||||
const savedAnimations = localStorage.getItem('animationsEnabled');
|
||||
this.animationsEnabled = savedAnimations !== null ? savedAnimations === 'true' : true;
|
||||
|
||||
// Инициализируем режим Про
|
||||
const savedProMode = localStorage.getItem('proModeEnabled');
|
||||
this.proModeEnabled = savedProMode === 'true';
|
||||
|
||||
// Инициализируем ночной режим
|
||||
const savedNightMode = localStorage.getItem('nightModeEnabled');
|
||||
this.nightModeEnabled = savedNightMode === 'true';
|
||||
if (this.nightModeEnabled) {
|
||||
document.body.classList.add('night-mode');
|
||||
}
|
||||
|
||||
// Инициализируем белые масти
|
||||
const savedWhiteSuits = localStorage.getItem('whiteSuitsEnabled');
|
||||
this.whiteSuitsEnabled = savedWhiteSuits === 'true';
|
||||
|
||||
// Показываем настройку белых мастей только если ночной режим включен
|
||||
if (this.nightModeEnabled && this.whiteSuitsSetting) {
|
||||
this.whiteSuitsSetting.style.display = 'flex';
|
||||
// Применяем белые масти если они были включены
|
||||
if (this.whiteSuitsEnabled) {
|
||||
document.body.classList.add('white-suits');
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализируем цвет рубашки карт
|
||||
const savedCardBack = localStorage.getItem('cardBackColor') || 'gray';
|
||||
this.cardBackColor = savedCardBack;
|
||||
document.body.setAttribute('data-card-back', savedCardBack);
|
||||
|
||||
// Инициализируем цвет фона
|
||||
const savedBackground = localStorage.getItem('backgroundColor') || 'default';
|
||||
this.backgroundColor = savedBackground;
|
||||
document.body.setAttribute('data-background', savedBackground);
|
||||
|
||||
// Обновляем иконку кнопки ночного режима
|
||||
this.updateNightModeButton();
|
||||
}
|
||||
|
||||
initEventListeners() {
|
||||
@@ -273,6 +561,11 @@ class CardGame {
|
||||
this.shareLinkBtn.style.display = 'block';
|
||||
}
|
||||
|
||||
// Night mode toggle button on lobby
|
||||
if (this.nightModeToggleBtn) {
|
||||
this.nightModeToggleBtn.addEventListener('click', () => this.toggleNightModeFromButton());
|
||||
}
|
||||
|
||||
this.readyToggleBtn.addEventListener('click', () => this.toggleReady());
|
||||
this.leaveRoomBtn.addEventListener('click', () => this.leaveRoom());
|
||||
|
||||
@@ -293,6 +586,9 @@ class CardGame {
|
||||
this.fullscreenToggle.addEventListener('change', () => this.toggleFullscreen());
|
||||
this.soundToggle.addEventListener('change', () => this.toggleSound());
|
||||
this.animationsToggle.addEventListener('change', () => this.toggleAnimations());
|
||||
this.proModeToggle.addEventListener('change', () => this.toggleProMode());
|
||||
this.nightModeToggle.addEventListener('change', () => this.toggleNightMode());
|
||||
this.whiteSuitsToggle.addEventListener('change', () => this.toggleWhiteSuits());
|
||||
this.logToggle.addEventListener('change', () => this.toggleLog());
|
||||
|
||||
// Leave game button
|
||||
@@ -303,7 +599,7 @@ class CardGame {
|
||||
// Suit selection
|
||||
document.querySelectorAll('.suit-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const suit = e.target.dataset.suit;
|
||||
const suit = e.currentTarget.dataset.suit;
|
||||
this.selectSuit(suit);
|
||||
});
|
||||
});
|
||||
@@ -329,6 +625,49 @@ 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();
|
||||
}
|
||||
});
|
||||
|
||||
// Card back color selector
|
||||
document.querySelectorAll('.card-back-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => {
|
||||
const backColor = e.currentTarget.dataset.back;
|
||||
this.setCardBackColor(backColor);
|
||||
});
|
||||
});
|
||||
|
||||
// Background color selector
|
||||
document.querySelectorAll('.background-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => {
|
||||
const bgColor = e.currentTarget.dataset.bg;
|
||||
this.setBackgroundColor(bgColor);
|
||||
});
|
||||
});
|
||||
|
||||
// Rules handlers
|
||||
this.rulesBtn.addEventListener('click', () => this.openRules());
|
||||
this.closeRulesBtn.addEventListener('click', () => this.closeRules());
|
||||
@@ -353,6 +692,38 @@ class CardGame {
|
||||
this.pendingCardToPlay = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Закрытие модалок по клику на фон
|
||||
this.setupModalBackdropClose();
|
||||
}
|
||||
|
||||
setupModalBackdropClose() {
|
||||
// Список всех модалок
|
||||
const modals = [
|
||||
{ element: this.settingsModal, closeMethod: () => this.closeSettings() },
|
||||
{ element: this.resultsModal, closeMethod: () => this.closeResultsModal() },
|
||||
{ element: this.rulesModal, closeMethod: () => this.closeRules() },
|
||||
{ element: this.alertModal, closeMethod: () => this.alertModal.classList.remove('active') },
|
||||
{ element: this.suitModal, closeMethod: () => {
|
||||
this.suitModal.classList.remove('active');
|
||||
this.pendingCardToPlay = null;
|
||||
}},
|
||||
{ element: this.chatModal, closeMethod: () => this.chatModal.classList.remove('active') },
|
||||
{ element: this.joinModal, closeMethod: () => this.joinModal.classList.remove('active') },
|
||||
{ element: this.leaveConfirmModal, closeMethod: () => this.leaveConfirmModal.classList.remove('active') }
|
||||
];
|
||||
|
||||
// Добавляем обработчик для каждой модалки
|
||||
modals.forEach(modal => {
|
||||
if (modal.element) {
|
||||
modal.element.addEventListener('click', (e) => {
|
||||
// Закрываем только если клик был по самой модалке (фону), а не по её содержимому
|
||||
if (e.target === modal.element) {
|
||||
modal.closeMethod();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
connect(reconnect = false) {
|
||||
@@ -531,7 +902,7 @@ class CardGame {
|
||||
this.animateDrawCards(data.forced_draw_player_id, data.forced_draw_count);
|
||||
}, 300);
|
||||
|
||||
const cardName = `${data.top_card.rank}${this.getSuitSymbol(data.top_card.suit)}`;
|
||||
const cardName = `${data.top_card.rank}${this.getSuitSymbolForLog(data.top_card.suit)}`;
|
||||
const cardsText = data.forced_draw_count === 1 ? '1 карту' : `${data.forced_draw_count} карты`;
|
||||
this.addLogEntry(`${data.forced_draw_player_nickname} взял ${cardsText} от ${cardName}`);
|
||||
}
|
||||
@@ -588,7 +959,7 @@ class CardGame {
|
||||
}
|
||||
|
||||
// Логируем событие
|
||||
const cardName = `${data.card.rank}${this.getSuitSymbol(data.card.suit)}`;
|
||||
const cardName = `${data.card.rank} ${this.getSuitSymbolForLog(data.card.suit)}`;
|
||||
this.addLogEntry(`${data.player_nickname} сыграл ${cardName}`);
|
||||
|
||||
// Если выбрана масть дамой
|
||||
@@ -634,7 +1005,7 @@ class CardGame {
|
||||
const cardsText = data.cards_count === 1 ? '1 карту' : `${data.cards_count} карты`;
|
||||
if (data.waiting_for_eight) {
|
||||
const topCard = data.top_card;
|
||||
this.addLogEntry(`${data.player_nickname} взял ${cardsText} от ${topCard.rank}${this.getSuitSymbol(topCard.suit)}`);
|
||||
this.addLogEntry(`${data.player_nickname} взял ${cardsText} от ${topCard.rank} ${this.getSuitSymbolForLog(topCard.suit)}`);
|
||||
} else {
|
||||
const cardsText = data.cards_count === 1 ? 'карту' : 'карты';
|
||||
this.addLogEntry(`${data.player_nickname} взял ${data.cards_count} ${cardsText}`);
|
||||
@@ -688,7 +1059,12 @@ class CardGame {
|
||||
chatClass = `chat-other chat-color-${colorIndex}`;
|
||||
}
|
||||
|
||||
// В лог пишем с ником
|
||||
this.addLogEntry(data.message, chatClass);
|
||||
|
||||
// В пузыре показываем только текст без ника
|
||||
const bubbleText = data.message_text || data.message;
|
||||
this.showChatBubble(data.player_id, bubbleText);
|
||||
}
|
||||
break;
|
||||
case 'game_ended':
|
||||
@@ -733,6 +1109,18 @@ class CardGame {
|
||||
// Игрок переподключился
|
||||
this.addLogEntry(`${data.nickname} переподключился`);
|
||||
break;
|
||||
case 'shake_discard':
|
||||
// Тряска карты сброса
|
||||
this.animateShakeDiscard();
|
||||
this.playSound('alert');
|
||||
break;
|
||||
case 'reaction':
|
||||
// Быстрая реакция от игрока
|
||||
// Используем маппинг звуков для реакций
|
||||
const soundName = this.reactionSounds[data.emoji] || 'chat';
|
||||
this.playSound(soundName);
|
||||
this.showReactionBubble(data.player_id, data.emoji);
|
||||
break;
|
||||
case 'deck_size_changed':
|
||||
// Размер колоды изменён
|
||||
if (this.currentRoom) {
|
||||
@@ -956,6 +1344,11 @@ class CardGame {
|
||||
}
|
||||
|
||||
leaveRoom() {
|
||||
// Отправляем сообщение о выходе из комнаты
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.send({ type: 'leave_room' });
|
||||
}
|
||||
|
||||
// Скрываем блок с ссылкой если он был показан
|
||||
if (this.inviteLinkBlock) {
|
||||
this.inviteLinkBlock.style.display = 'none';
|
||||
@@ -976,21 +1369,24 @@ class CardGame {
|
||||
}
|
||||
|
||||
updateRoomSettings(room) {
|
||||
// Показываем настройки только создателю комнаты и только до начала игры
|
||||
// Показываем настройки только создателю комнаты и только до начала первого раунда
|
||||
const isCreator = room.creator_id === this.playerId;
|
||||
const gameNotStarted = !room.game_started;
|
||||
|
||||
// Проверяем что ни у кого нет очков (игра ещё не начиналась)
|
||||
// Проверяем что ни у кого нет очков (игра ещё не начиналась ни разу)
|
||||
// Это ключевая проверка - между раундами game_started=false, но очки уже есть
|
||||
const noScores = room.players.every(p => p.score === 0);
|
||||
|
||||
// Проверяем есть ли боты в комнате
|
||||
const hasBot = room.players.some(p => p.is_bot);
|
||||
|
||||
// Настройки комнаты (переключатель 36/52) показываем только до первого раунда
|
||||
const shouldShow = isCreator && gameNotStarted && noScores;
|
||||
|
||||
this.roomSettings.style.display = shouldShow ? 'block' : 'none';
|
||||
|
||||
// Блок приватной комнаты показываем только если нет ботов
|
||||
// Блок приватной комнаты показываем ТОЛЬКО при создании комнаты (noScores) и без ботов
|
||||
// Между раундами он не должен показываться, даже если game_started=false
|
||||
if (this.privateRoomSettings) {
|
||||
this.privateRoomSettings.style.display = (shouldShow && !hasBot) ? 'block' : 'none';
|
||||
}
|
||||
@@ -1019,6 +1415,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);
|
||||
}
|
||||
|
||||
@@ -1026,7 +1424,9 @@ class CardGame {
|
||||
// Показываем индикатор всегда когда есть выбранная масть (после дамы)
|
||||
if (data.chosen_suit) {
|
||||
this.chosenSuitIndicator.style.display = 'block';
|
||||
this.chosenSuitIndicator.textContent = this.getSuitSymbol(data.chosen_suit);
|
||||
const suitEmoji = this.getSuitSymbol(data.chosen_suit);
|
||||
const suitClass = data.chosen_suit; // hearts, diamonds, clubs, spades
|
||||
this.chosenSuitIndicator.innerHTML = `<span class="suit-emoji-indicator ${suitClass}">${suitEmoji}</span>`;
|
||||
} else {
|
||||
this.chosenSuitIndicator.style.display = 'none';
|
||||
}
|
||||
@@ -1178,6 +1578,10 @@ class CardGame {
|
||||
}
|
||||
|
||||
updateHand(topCard, chosenSuit) {
|
||||
// Сохраняем для использования при переключении режима Про
|
||||
this.topCard = topCard;
|
||||
this.chosenSuit = chosenSuit;
|
||||
|
||||
this.handCards.innerHTML = '';
|
||||
|
||||
this.hand.forEach(card => {
|
||||
@@ -1188,8 +1592,17 @@ class CardGame {
|
||||
this.canPlayCard(card, topCard, chosenSuit, this.waitingForEight, this.eightDrawnCards);
|
||||
|
||||
if (!canPlay) {
|
||||
cardElement.classList.add('disabled');
|
||||
// В режиме Про карты остаются яркими (не добавляем disabled)
|
||||
// В обычном режиме затемняем неподходящие карты
|
||||
if (this.proModeEnabled) {
|
||||
cardElement.classList.remove('disabled');
|
||||
// В режиме Про добавляем обработчик клика даже для неподходящих карт
|
||||
cardElement.addEventListener('click', () => this.playCard(card));
|
||||
} else {
|
||||
cardElement.classList.add('disabled');
|
||||
}
|
||||
} else {
|
||||
cardElement.classList.remove('disabled');
|
||||
cardElement.addEventListener('click', () => this.playCard(card));
|
||||
}
|
||||
|
||||
@@ -1252,6 +1665,16 @@ class CardGame {
|
||||
return symbols[suit] || '';
|
||||
}
|
||||
|
||||
getSuitSymbolForLog(suit) {
|
||||
const symbols = {
|
||||
'hearts': '<span class="suit-emoji-log hearts">♥️</span>',
|
||||
'diamonds': '<span class="suit-emoji-log diamonds">♦️</span>',
|
||||
'clubs': '<span class="suit-emoji-log clubs">♣️</span>',
|
||||
'spades': '<span class="suit-emoji-log spades">♠️</span>'
|
||||
};
|
||||
return symbols[suit] || '';
|
||||
}
|
||||
|
||||
getSuitName(suit) {
|
||||
const names = {
|
||||
'hearts': 'черви',
|
||||
@@ -1320,7 +1743,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) {
|
||||
@@ -1351,8 +1795,34 @@ class CardGame {
|
||||
// Синхронизируем состояние переключателей с текущими настройками
|
||||
this.soundToggle.checked = this.soundEnabled;
|
||||
this.animationsToggle.checked = this.animationsEnabled;
|
||||
this.proModeToggle.checked = this.proModeEnabled;
|
||||
this.nightModeToggle.checked = this.nightModeEnabled;
|
||||
this.whiteSuitsToggle.checked = this.whiteSuitsEnabled;
|
||||
this.fullscreenToggle.checked = !!document.fullscreenElement;
|
||||
|
||||
// Показываем/скрываем настройку белых мастей в зависимости от ночного режима
|
||||
if (this.whiteSuitsSetting) {
|
||||
this.whiteSuitsSetting.style.display = this.nightModeEnabled ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
// Синхронизируем выбранный цвет рубашки
|
||||
document.querySelectorAll('.card-back-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
});
|
||||
const selectedOption = document.querySelector(`.card-back-option[data-back="${this.cardBackColor}"]`);
|
||||
if (selectedOption) {
|
||||
selectedOption.classList.add('selected');
|
||||
}
|
||||
|
||||
// Синхронизируем выбранный цвет фона
|
||||
document.querySelectorAll('.background-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
});
|
||||
const selectedBgOption = document.querySelector(`.background-option[data-bg="${this.backgroundColor}"]`);
|
||||
if (selectedBgOption) {
|
||||
selectedBgOption.classList.add('selected');
|
||||
}
|
||||
|
||||
// Синхронизируем состояние лога (проверяем есть ли класс hidden)
|
||||
if (this.gameLog) {
|
||||
this.logToggle.checked = !this.gameLog.classList.contains('hidden');
|
||||
@@ -1380,7 +1850,7 @@ class CardGame {
|
||||
|
||||
const entry = document.createElement('div');
|
||||
entry.className = extraClass ? `log-entry ${extraClass}` : 'log-entry';
|
||||
entry.textContent = message;
|
||||
entry.innerHTML = message;
|
||||
|
||||
// Добавляем в начало (новые сверху)
|
||||
this.gameLog.insertBefore(entry, this.gameLog.firstChild);
|
||||
@@ -1470,9 +1940,15 @@ class CardGame {
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
// Текст для обнуления при 101
|
||||
const resetText = result.reset_to_zero
|
||||
? `<p style="color: #ff9800; font-weight: bold; font-size: 1.1em;">🎯 Ровно 101! Очки обнулены!</p>`
|
||||
: '';
|
||||
|
||||
resultItem.innerHTML = `
|
||||
<h4>${result.nickname}</h4>
|
||||
<p>Очки за раунд: +${result.points}</p>
|
||||
${resetText}
|
||||
<p>Всего очков: ${result.total_score}</p>
|
||||
${cardsHtml}
|
||||
`;
|
||||
@@ -1502,6 +1978,12 @@ class CardGame {
|
||||
closeResultsModal() {
|
||||
this.resultsModal.classList.remove('active');
|
||||
this.showScreen('room');
|
||||
|
||||
// Обновляем настройки комнаты чтобы скрыть блок приватной комнаты после первого раунда
|
||||
if (this.currentRoom) {
|
||||
this.updateRoomSettings(this.currentRoom);
|
||||
}
|
||||
|
||||
// Удаляем обработчик Enter
|
||||
if (this.resultsEnterHandler) {
|
||||
document.removeEventListener('keydown', this.resultsEnterHandler);
|
||||
@@ -1535,6 +2017,205 @@ class CardGame {
|
||||
this.closeChat();
|
||||
}
|
||||
|
||||
showReactionPicker(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!this.reactionPicker) return;
|
||||
|
||||
// Проверяем кулдаун (5 секунд)
|
||||
const now = Date.now();
|
||||
const cooldown = 5000; // 5 секунд в миллисекундах
|
||||
|
||||
if (now - this.lastReactionTime < cooldown) {
|
||||
// Просто игнорируем клик если кулдаун активен
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем координаты блока руки (.player-hand)
|
||||
const playerHand = document.querySelector('.player-hand');
|
||||
if (!playerHand) return;
|
||||
|
||||
const rect = playerHand.getBoundingClientRect();
|
||||
|
||||
// Позиционируем пикер НАД блоком руки по центру
|
||||
const vh = window.innerHeight / 100;
|
||||
this.reactionPicker.style.left = `${rect.left + rect.width / 2}px`;
|
||||
this.reactionPicker.style.transform = 'translateX(-50%)';
|
||||
this.reactionPicker.style.top = `${rect.top - 70 - vh}px`; // Над рукой + 1vh выше
|
||||
|
||||
this.reactionPicker.classList.add('active');
|
||||
|
||||
// Предотвращаем конфликт свайпа с другими элементами
|
||||
const scrollContainer = this.reactionPicker.querySelector('.reaction-picker-scroll');
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener('touchstart', (e) => {
|
||||
e.stopPropagation();
|
||||
}, { passive: true, once: false });
|
||||
|
||||
scrollContainer.addEventListener('touchmove', (e) => {
|
||||
e.stopPropagation();
|
||||
}, { passive: true, once: false });
|
||||
|
||||
// Drag-скролл мышкой (как на телефоне)
|
||||
let isDown = false;
|
||||
let startX;
|
||||
let scrollLeft;
|
||||
let hasMoved = false;
|
||||
|
||||
scrollContainer.addEventListener('mousedown', (e) => {
|
||||
isDown = true;
|
||||
hasMoved = false;
|
||||
scrollContainer.style.scrollBehavior = 'auto';
|
||||
startX = e.pageX;
|
||||
scrollLeft = scrollContainer.scrollLeft;
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener('mouseleave', () => {
|
||||
isDown = false;
|
||||
scrollContainer.style.scrollBehavior = 'smooth';
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener('mouseup', () => {
|
||||
isDown = false;
|
||||
scrollContainer.style.scrollBehavior = 'smooth';
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener('mousemove', (e) => {
|
||||
if (!isDown) return;
|
||||
e.preventDefault();
|
||||
const x = e.pageX;
|
||||
const walk = x - startX;
|
||||
|
||||
// Если сдвинули больше чем на 5px, считаем это скроллом
|
||||
if (Math.abs(walk) > 5) {
|
||||
hasMoved = true;
|
||||
}
|
||||
|
||||
scrollContainer.scrollLeft = scrollLeft - walk;
|
||||
});
|
||||
|
||||
// Предотвращаем клик на кнопках если был скролл
|
||||
scrollContainer.addEventListener('click', (e) => {
|
||||
if (hasMoved) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
hasMoved = false;
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Скролл колёсиком мыши по горизонтали
|
||||
scrollContainer.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
scrollContainer.scrollLeft += e.deltaY * 2.42; // Ускоряем скролл в 2 раза
|
||||
}, { passive: false });
|
||||
}
|
||||
}
|
||||
|
||||
hideReactionPicker() {
|
||||
if (this.reactionPicker) {
|
||||
this.reactionPicker.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
sendReaction(emoji) {
|
||||
// Обновляем время последней реакции
|
||||
this.lastReactionTime = Date.now();
|
||||
|
||||
this.send({
|
||||
type: 'reaction',
|
||||
emoji: emoji
|
||||
});
|
||||
}
|
||||
|
||||
showReactionBubble(playerId, emoji) {
|
||||
// Находим область игрока
|
||||
let targetElement;
|
||||
if (playerId === this.playerId) {
|
||||
// Реакция от нас самих - показываем над нашими картами
|
||||
targetElement = this.handCards;
|
||||
} else {
|
||||
// Реакция от противника - находим блок с ником и очками
|
||||
const opponentArea = this.getOpponentAreaById(playerId);
|
||||
if (opponentArea) {
|
||||
// Берём блок opponent-info (ник и очки) для центрирования
|
||||
targetElement = opponentArea.querySelector('.opponent-info');
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetElement) return;
|
||||
|
||||
// Создаём пузырёк
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'reaction-bubble';
|
||||
bubble.textContent = emoji;
|
||||
|
||||
// Позиционируем пузырёк и добавляем стрелочку
|
||||
const rect = targetElement.getBoundingClientRect();
|
||||
bubble.style.left = `${rect.left + rect.width / 2}px`; // Центр элемента
|
||||
bubble.style.transform = 'translateX(-50%)'; // Центрируем пузырёк
|
||||
|
||||
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);
|
||||
|
||||
// Удаляем пузырёк после анимации (3 секунды для реакций)
|
||||
setTimeout(() => {
|
||||
bubble.remove();
|
||||
}, 4200);
|
||||
}
|
||||
|
||||
showChatBubble(playerId, message) {
|
||||
// Находим область игрока
|
||||
let targetElement;
|
||||
if (playerId === this.playerId) {
|
||||
// Сообщение от нас - показываем над нашими картами
|
||||
targetElement = this.handCards;
|
||||
} else {
|
||||
// Сообщение от противника - находим блок с ником и очками
|
||||
const opponentArea = this.getOpponentAreaById(playerId);
|
||||
if (opponentArea) {
|
||||
targetElement = opponentArea.querySelector('.opponent-info');
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetElement) return;
|
||||
|
||||
// Создаём пузырёк для сообщения
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'reaction-bubble chat-bubble';
|
||||
bubble.textContent = message;
|
||||
|
||||
// Позиционируем пузырёк
|
||||
const rect = targetElement.getBoundingClientRect();
|
||||
bubble.style.left = `${rect.left + rect.width / 2}px`;
|
||||
bubble.style.transform = 'translateX(-50%)';
|
||||
|
||||
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);
|
||||
|
||||
// Удаляем пузырёк после 5 секунд (дольше чем реакции)
|
||||
setTimeout(() => {
|
||||
bubble.remove();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
handleKeyPress(e) {
|
||||
// Проверяем что мы не в поле ввода (кроме чата)
|
||||
const isInInput = e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA';
|
||||
@@ -1788,6 +2469,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 для разных цветов
|
||||
|
||||
+630
@@ -0,0 +1,630 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<title>Чешский</title>
|
||||
<!-- Раскомментируйте и укажите ваш WebSocket домен если используете reverse proxy -->
|
||||
<!-- <meta name="ws-host" content="ws.domain.com"> -->
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png">
|
||||
<link rel="shortcut icon" href="/icon-192.png">
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#667eea">
|
||||
|
||||
<!-- iOS Icons -->
|
||||
<link rel="apple-touch-icon" href="/icon-192.png">
|
||||
|
||||
<link rel="stylesheet" href="/style.css?v=3">
|
||||
<link rel="stylesheet" href="/night-mode.css?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Экран лобби -->
|
||||
<div id="lobby-screen" class="screen active">
|
||||
<div class="container">
|
||||
<div class="lobby-header">
|
||||
<h1>🎴 Чешский</h1>
|
||||
<button id="night-mode-toggle-btn" class="night-mode-btn" title="Переключить ночной режим">
|
||||
<span class="sun-icon" style="display: none;">☀️</span>
|
||||
<span class="moon-icon">🌙</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="create-room-section">
|
||||
<h2>Создать комнату</h2>
|
||||
<input type="text" id="nickname-input" placeholder="Введите никнейм" maxlength="20">
|
||||
<button id="create-room-btn" class="btn btn-primary">Создать комнату</button>
|
||||
|
||||
<div id="bot-game-section">
|
||||
<h3>🤖 Играть с ботами</h3>
|
||||
<div class="bot-buttons">
|
||||
<button id="play-with-1-bot-btn" class="btn btn-bot">Играть с 1 ботом</button>
|
||||
<button id="play-with-2-bots-btn" class="btn btn-bot">Играть с 2 ботами</button>
|
||||
<button id="play-with-3-bots-btn" class="btn btn-bot">Играть с 3 ботами</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rooms-section">
|
||||
<h2>Доступные комнаты</h2>
|
||||
<div id="rooms-list"></div>
|
||||
</div>
|
||||
|
||||
<div id="rules-section">
|
||||
<h2>📖 Правила игры</h2>
|
||||
<div id="lobby-rules-content" class="rules-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Экран комнаты -->
|
||||
<div id="room-screen" class="screen">
|
||||
<div class="container">
|
||||
<div class="room-header">
|
||||
<h2>Комната</h2>
|
||||
<!-- Таймер в заголовке -->
|
||||
<div id="countdown-display" style="display: none;">
|
||||
<span class="countdown-timer-small" id="countdown-number">6</span>
|
||||
</div>
|
||||
<button id="leave-room-btn" class="btn btn-secondary">Покинуть</button>
|
||||
</div>
|
||||
|
||||
<div id="players-waiting">
|
||||
<h3>Игроки в комнате:</h3>
|
||||
<p class="hint">Игра начнётся когда все игроки будут готовы (минимум 2 игрока)</p>
|
||||
<div id="players-list"></div>
|
||||
|
||||
<!-- Кнопка готов после списка игроков -->
|
||||
<button id="ready-toggle-btn" class="btn btn-primary">Готов</button>
|
||||
|
||||
<!-- Настройки комнаты (только для создателя) -->
|
||||
<div id="room-settings" style="display: none;">
|
||||
<h3>Настройки игры</h3>
|
||||
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">36 карт</span>
|
||||
<input type="checkbox" id="deck-size-toggle" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">52 карты</span>
|
||||
</label>
|
||||
<p class="hint-small">В режиме 36 карт восьмёрка не действует</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Переключатель приватной/публичной комнаты -->
|
||||
<div class="room-settings" id="private-room-settings">
|
||||
<!-- Переключатель приватной комнаты -->
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Публичная</span>
|
||||
<input type="checkbox" id="private-room-toggle">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Приватная</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Блок с ссылкой для приглашения (скрыт по умолчанию) -->
|
||||
<div id="invite-link-block" style="display: none;">
|
||||
<p class="invite-hint">Поделитесь ссылкой с друзьями:</p>
|
||||
<div class="invite-link-container">
|
||||
<code id="invite-link" class="invite-link"></code>
|
||||
<button id="copy-link-btn" class="btn btn-copy" title="Копировать ссылку"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 1.25H10.9436C9.10583 1.24998 7.65019 1.24997 6.51098 1.40314C5.33856 1.56076 4.38961 1.89288 3.64124 2.64124C2.89288 3.38961 2.56076 4.33856 2.40314 5.51098C2.24997 6.65019 2.24998 8.10582 2.25 9.94357V16C2.25 17.8722 3.62205 19.424 5.41551 19.7047C5.55348 20.4687 5.81753 21.1208 6.34835 21.6517C6.95027 22.2536 7.70814 22.5125 8.60825 22.6335C9.47522 22.75 10.5775 22.75 11.9451 22.75H15.0549C16.4225 22.75 17.5248 22.75 18.3918 22.6335C19.2919 22.5125 20.0497 22.2536 20.6517 21.6517C21.2536 21.0497 21.5125 20.2919 21.6335 19.3918C21.75 18.5248 21.75 17.4225 21.75 16.0549V10.9451C21.75 9.57754 21.75 8.47522 21.6335 7.60825C21.5125 6.70814 21.2536 5.95027 20.6517 5.34835C20.1208 4.81753 19.4687 4.55348 18.7047 4.41551C18.424 2.62205 16.8722 1.25 15 1.25ZM17.1293 4.27117C16.8265 3.38623 15.9876 2.75 15 2.75H11C9.09318 2.75 7.73851 2.75159 6.71085 2.88976C5.70476 3.02502 5.12511 3.27869 4.7019 3.7019C4.27869 4.12511 4.02502 4.70476 3.88976 5.71085C3.75159 6.73851 3.75 8.09318 3.75 10V16C3.75 16.9876 4.38624 17.8265 5.27117 18.1293C5.24998 17.5194 5.24999 16.8297 5.25 16.0549V10.9451C5.24998 9.57754 5.24996 8.47522 5.36652 7.60825C5.48754 6.70814 5.74643 5.95027 6.34835 5.34835C6.95027 4.74643 7.70814 4.48754 8.60825 4.36652C9.47522 4.24996 10.5775 4.24998 11.9451 4.25H15.0549C15.8297 4.24999 16.5194 4.24998 17.1293 4.27117ZM7.40901 6.40901C7.68577 6.13225 8.07435 5.9518 8.80812 5.85315C9.56347 5.75159 10.5646 5.75 12 5.75H15C16.4354 5.75 17.4365 5.75159 18.1919 5.85315C18.9257 5.9518 19.3142 6.13225 19.591 6.40901C19.8678 6.68577 20.0482 7.07435 20.1469 7.80812C20.2484 8.56347 20.25 9.56458 20.25 11V16C20.25 17.4354 20.2484 18.4365 20.1469 19.1919C20.0482 19.9257 19.8678 20.3142 19.591 20.591C19.3142 20.8678 18.9257 21.0482 18.1919 21.1469C17.4365 21.2484 16.4354 21.25 15 21.25H12C10.5646 21.25 9.56347 21.2484 8.80812 21.1469C8.07435 21.0482 7.68577 20.8678 7.40901 20.591C7.13225 20.3142 6.9518 19.9257 6.85315 19.1919C6.75159 18.4365 6.75 17.4354 6.75 16V11C6.75 9.56458 6.75159 8.56347 6.85315 7.80812C6.9518 7.07435 7.13225 6.68577 7.40901 6.40901Z" fill="#FFFFFF"/>
|
||||
</svg></button>
|
||||
<button id="share-link-btn" class="btn btn-share" title="Поделиться" style="display: none;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 1.25H11.9426C9.63423 1.24999 7.82519 1.24998 6.41371 1.43975C4.96897 1.63399 3.82895 2.03933 2.93414 2.93414C2.03933 3.82895 1.63399 4.96897 1.43975 6.41371C1.24998 7.82519 1.24999 9.63423 1.25 11.9426V12.0574C1.24999 14.3658 1.24998 16.1748 1.43975 17.5863C1.63399 19.031 2.03933 20.1711 2.93414 21.0659C3.82895 21.9607 4.96897 22.366 6.41371 22.5603C7.82519 22.75 9.63423 22.75 11.9426 22.75H12.0574C14.3658 22.75 16.1748 22.75 17.5863 22.5603C19.031 22.366 20.1711 21.9607 21.0659 21.0659C21.9607 20.1711 22.366 19.031 22.5603 17.5863C22.75 16.1748 22.75 14.3658 22.75 12.0574V12C22.75 11.5858 22.4142 11.25 22 11.25C21.5858 11.25 21.25 11.5858 21.25 12C21.25 14.3782 21.2484 16.0864 21.0736 17.3864C20.9018 18.6648 20.5749 19.4355 20.0052 20.0052C19.4355 20.5749 18.6648 20.9018 17.3864 21.0736C16.0864 21.2484 14.3782 21.25 12 21.25C9.62178 21.25 7.91356 21.2484 6.61358 21.0736C5.33517 20.9018 4.56445 20.5749 3.9948 20.0052C3.42514 19.4355 3.09825 18.6648 2.92637 17.3864C2.75159 16.0864 2.75 14.3782 2.75 12C2.75 9.62178 2.75159 7.91356 2.92637 6.61358C3.09825 5.33517 3.42514 4.56445 3.9948 3.9948C4.56445 3.42514 5.33517 3.09825 6.61358 2.92637C7.91356 2.75159 9.62178 2.75 12 2.75C12.4142 2.75 12.75 2.41421 12.75 2C12.75 1.58579 12.4142 1.25 12 1.25Z" fill="#FFFFFF"/>
|
||||
<path d="M12.4697 10.4697C12.1768 10.7626 12.1768 11.2374 12.4697 11.5303C12.7626 11.8232 13.2374 11.8232 13.5303 11.5303L21.25 3.81066V7.34375C21.25 7.75796 21.5858 8.09375 22 8.09375C22.4142 8.09375 22.75 7.75796 22.75 7.34375V2C22.75 1.58579 22.4142 1.25 22 1.25H16.6562C16.242 1.25 15.9062 1.58579 15.9062 2C15.9062 2.41421 16.242 2.75 16.6562 2.75H20.1893L12.4697 10.4697Z" fill="#FFFFFF"/>
|
||||
</svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Экран ошибки -->
|
||||
<div id="error-screen" class="screen">
|
||||
<div class="error-container">
|
||||
<div class="error-content">
|
||||
<h1>😕</h1>
|
||||
<h2>Комната не найдена</h2>
|
||||
<p id="error-message">Эта комната не существует или была удалена</p>
|
||||
<button id="go-home-btn" class="btn btn-primary">На главную</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Экран игры -->
|
||||
<div id="game-screen" class="screen">
|
||||
<div class="game-container">
|
||||
<!-- Кнопка покинуть комнату -->
|
||||
<button id="leave-game-btn" class="leave-game-btn" title="Покинуть комнату">
|
||||
<span class="leave-icon">🚪</span>
|
||||
</button>
|
||||
|
||||
<!-- Кнопка настроек -->
|
||||
<button id="settings-btn" class="settings-btn" title="Настройки">⚙️</button>
|
||||
|
||||
<!-- Кнопка правил -->
|
||||
<button id="rules-btn" class="rules-btn" title="Правила игры">📖</button>
|
||||
|
||||
<!-- Кнопка чата -->
|
||||
<button id="chat-btn" class="chat-btn" title="Отправить сообщение">💬</button>
|
||||
|
||||
<!-- Круговое игровое поле -->
|
||||
<div class="game-table">
|
||||
<!-- Игровой лог -->
|
||||
<div id="game-log" class="game-log"></div>
|
||||
|
||||
<!-- Контейнер для противников -->
|
||||
<div id="opponents-container">
|
||||
<!-- Игрок слева -->
|
||||
<div class="opponent-area opponent-left" id="opponent-left">
|
||||
<div class="opponent-info">
|
||||
<span class="opponent-name"></span>
|
||||
<span class="opponent-score"></span>
|
||||
</div>
|
||||
<div class="opponent-cards"></div>
|
||||
</div>
|
||||
|
||||
<!-- Игрок по центру -->
|
||||
<div class="opponent-area opponent-top" id="opponent-top">
|
||||
<div class="opponent-info">
|
||||
<span class="opponent-name"></span>
|
||||
<span class="opponent-score"></span>
|
||||
</div>
|
||||
<div class="opponent-cards"></div>
|
||||
</div>
|
||||
|
||||
<!-- Игрок справа -->
|
||||
<div class="opponent-area opponent-right" id="opponent-right">
|
||||
<div class="opponent-info">
|
||||
<span class="opponent-name"></span>
|
||||
<span class="opponent-score"></span>
|
||||
</div>
|
||||
<div class="opponent-cards"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Центральный круг с картой и колодой -->
|
||||
<div class="game-circle">
|
||||
<div class="center-area">
|
||||
<div class="center-card-wrapper">
|
||||
<!-- Индикатор выбранной масти -->
|
||||
<div id="chosen-suit-indicator" style="display: none;"></div>
|
||||
<div id="discard-pile" class="center-card"></div>
|
||||
</div>
|
||||
<div class="deck-area">
|
||||
<div class="deck-count-badge" id="deck-count">0</div>
|
||||
<div id="deck" class="card-pile">
|
||||
<div class="card card-back">🎴</div>
|
||||
</div>
|
||||
<div class="deck-buttons">
|
||||
<button id="draw-card-btn" class="btn btn-primary btn-sm">Взять</button>
|
||||
<button id="skip-turn-btn" class="btn btn-secondary btn-sm" style="display: none;">Нету</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Контейнер игрока -->
|
||||
<div class="player-container">
|
||||
<!-- Информация об игроке -->
|
||||
<div class="player-info">
|
||||
<span class="player-name" id="player-name"></span>
|
||||
<span class="player-score" id="player-score"></span>
|
||||
</div>
|
||||
|
||||
<!-- Карты игрока внизу -->
|
||||
<div class="player-hand">
|
||||
<div id="hand-cards"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно чата -->
|
||||
<div id="chat-modal" class="modal">
|
||||
<div class="modal-content chat-modal-content">
|
||||
<h3>Отправить сообщение</h3>
|
||||
<textarea id="chat-input" placeholder="Введите сообщение..." maxlength="200"></textarea>
|
||||
<div class="chat-modal-buttons">
|
||||
<button id="send-chat-btn" class="btn btn-primary">Отправить</button>
|
||||
<button id="close-chat-btn" class="btn btn-secondary">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно быстрых реакций -->
|
||||
<div id="reaction-picker" class="reaction-picker">
|
||||
<div class="reaction-picker-scroll">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<button class="reaction-btn" data-reaction="♣️">♣️</button>
|
||||
<button class="reaction-btn" data-reaction="♠️">♠️</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно выбора масти -->
|
||||
<div id="suit-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Выберите масть</h3>
|
||||
<div class="suit-selector">
|
||||
<button class="suit-btn" data-suit="hearts"><span class="suit-emoji">♥️</span> Черви</button>
|
||||
<button class="suit-btn" data-suit="diamonds"><span class="suit-emoji">♦️</span> Бубны</button>
|
||||
<button class="suit-btn" data-suit="clubs"><span class="suit-emoji">♣️</span> Трефы</button>
|
||||
<button class="suit-btn" data-suit="spades"><span class="suit-emoji">♠️</span> Пики</button>
|
||||
</div>
|
||||
<div class="modal-buttons" style="margin-top: 15px;">
|
||||
<button id="cancel-suit-btn" class="btn btn-secondary">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно результатов -->
|
||||
<div id="results-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Игра окончена!</h2>
|
||||
<div id="results-content"></div>
|
||||
<button id="close-results-btn" class="btn btn-primary">Новая игра</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно присоединения к комнате -->
|
||||
<div id="join-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Присоединиться к комнате</h3>
|
||||
<input type="text" id="join-nickname-input" placeholder="Введите никнейм" maxlength="20">
|
||||
<div class="modal-buttons">
|
||||
<button id="join-confirm-btn" class="btn btn-primary">Присоединиться</button>
|
||||
<button id="join-cancel-btn" class="btn btn-secondary">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно оповещения -->
|
||||
<div id="alert-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Сообщение</h3>
|
||||
<p id="alert-text" style="margin: 10px 0 20px 0;"></p>
|
||||
<div class="modal-buttons">
|
||||
<button id="alert-ok-btn" class="btn btn-primary">ОК</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно подтверждения выхода -->
|
||||
<div id="leave-confirm-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Покинуть комнату?</h3>
|
||||
<p style="margin: 10px 0 20px 0;">Вы уверены, что хотите покинуть игру?</p>
|
||||
<div class="modal-buttons">
|
||||
<button id="leave-confirm-btn" class="btn btn-primary">Да, выйти</button>
|
||||
<button id="leave-cancel-btn" class="btn btn-secondary">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно правил -->
|
||||
<div id="rules-modal" class="modal">
|
||||
<div class="modal-content rules-modal-content">
|
||||
<button id="close-rules-btn" class="close-modal-btn">✕</button>
|
||||
<h2>📖 Правила игры</h2>
|
||||
<div id="rules-content" class="rules-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно настроек -->
|
||||
<div id="settings-modal" class="modal">
|
||||
<div class="modal-content settings-modal-content">
|
||||
<button id="close-settings-btn" class="close-modal-btn">✕</button>
|
||||
<h2>⚙️ Настройки</h2>
|
||||
<div class="settings-content">
|
||||
<!-- Полноэкранный режим -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">⛶ Полноэкранный режим</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
<input type="checkbox" id="fullscreen-toggle">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Вкл</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Звук -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">🔊 Звук</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
<input type="checkbox" id="sound-toggle" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Вкл</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Анимации -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">💫 Анимации карт</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
<input type="checkbox" id="animations-toggle" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Вкл</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Режим Про -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">🎓 Режим Про</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
<input type="checkbox" id="pro-mode-toggle">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Вкл</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ночной режим -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">🌙 Ночной режим</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
<input type="checkbox" id="night-mode-toggle">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Вкл</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Белые масти (только в ночном режиме) -->
|
||||
<div class="setting-item" id="white-suits-setting" style="display: none;">
|
||||
<span class="setting-label">🤍 Белые масти</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
<input type="checkbox" id="white-suits-toggle">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Вкл</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Лог (только на мобильных) -->
|
||||
<div class="setting-item mobile-only">
|
||||
<span class="setting-label">📃 Показывать лог</span>
|
||||
<div class="deck-size-toggle">
|
||||
<label>
|
||||
<span class="deck-label">Выкл</span>
|
||||
<input type="checkbox" id="log-toggle" checked>
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="deck-label">Вкл</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Выбор цвета рубашки карт -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">🎴 Цвет рубашки карт</span>
|
||||
<div class="card-back-selector">
|
||||
<div class="card-back-option" data-back="gray">
|
||||
<div class="card-back-preview card-back-gray"></div>
|
||||
</div>
|
||||
<div class="card-back-option" data-back="green">
|
||||
<div class="card-back-preview card-back-green"></div>
|
||||
</div>
|
||||
<div class="card-back-option" data-back="blue">
|
||||
<div class="card-back-preview card-back-blue"></div>
|
||||
</div>
|
||||
<div class="card-back-option" data-back="red">
|
||||
<div class="card-back-preview card-back-red"></div>
|
||||
</div>
|
||||
<div class="card-back-option" data-back="purple">
|
||||
<div class="card-back-preview card-back-purple"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Выбор цвета фона -->
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">🎨 Цвет фона</span>
|
||||
<div class="background-selector">
|
||||
<div class="background-option" data-bg="default">
|
||||
<div class="background-preview bg-default"></div>
|
||||
</div>
|
||||
<div class="background-option" data-bg="green">
|
||||
<div class="background-preview bg-green"></div>
|
||||
</div>
|
||||
<div class="background-option" data-bg="blue">
|
||||
<div class="background-preview bg-blue"></div>
|
||||
</div>
|
||||
<div class="background-option" data-bg="purple">
|
||||
<div class="background-preview bg-purple"></div>
|
||||
</div>
|
||||
<div class="background-option" data-bg="orange">
|
||||
<div class="background-preview bg-orange"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/marked.min.js?v=3"></script>
|
||||
<script src="/game.js?v=3"></script>
|
||||
|
||||
<!-- Service Worker Registration -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('ServiceWorker registered:', registration.scope);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('ServiceWorker registration failed:', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Функция проверки - находимся ли на главной странице (лобби)
|
||||
function isOnLobbyScreen() {
|
||||
const lobbyScreen = document.getElementById('lobby-screen');
|
||||
return lobbyScreen && lobbyScreen.classList.contains('active');
|
||||
}
|
||||
|
||||
// Функция проверки - находится ли элемент внутри блока руки
|
||||
function isInsidePlayerHand(element) {
|
||||
while (element) {
|
||||
if (element.classList && element.classList.contains('player-hand')) {
|
||||
return true;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Функция проверки - находится ли элемент внутри модалки с прокруткой
|
||||
function isInsideScrollableModal(element) {
|
||||
while (element) {
|
||||
if (element.classList && (
|
||||
element.classList.contains('rules-modal-content') ||
|
||||
element.classList.contains('modal-content')
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Отключаем pull-to-refresh только НЕ на главной странице
|
||||
let touchStartY = 0;
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
touchStartY = e.touches[0].clientY;
|
||||
}, { passive: false });
|
||||
|
||||
document.addEventListener('touchmove', function(e) {
|
||||
// Разрешаем все жесты на главной странице
|
||||
if (isOnLobbyScreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Разрешаем скролл внутри блока руки игрока
|
||||
if (isInsidePlayerHand(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Разрешаем скролл внутри модалок
|
||||
if (isInsideScrollableModal(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const touchY = e.touches[0].clientY;
|
||||
const touchDiff = touchY - touchStartY;
|
||||
|
||||
// Если пользователь тянет вниз на верху страницы - блокируем
|
||||
if (touchDiff > 0 && window.scrollY === 0) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Блокируем мультитач
|
||||
if (e.touches.length > 1) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// Отключаем двойное нажатие для зума только НЕ на главной
|
||||
let lastTouchEnd = 0;
|
||||
document.addEventListener('touchend', function(e) {
|
||||
// Разрешаем на главной странице
|
||||
if (isOnLobbyScreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastTouchEnd <= 300) {
|
||||
e.preventDefault();
|
||||
}
|
||||
lastTouchEnd = now;
|
||||
}, false);
|
||||
|
||||
// Предотвращаем обновление страницы через клавиатуру только НЕ на главной
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Разрешаем на главной странице
|
||||
if (isOnLobbyScreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.key === 'F5') ||
|
||||
(e.ctrlKey && e.key === 'r') ||
|
||||
(e.metaKey && e.key === 'r')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user