1782 lines
60 KiB
JavaScript
1782 lines
60 KiB
JavaScript
/**
|
||
* =============================================================================
|
||
* Texas Hold'em - Главный клиентский модуль
|
||
* UI, WebSocket, звуки, управление игрой
|
||
* =============================================================================
|
||
*/
|
||
|
||
// =============================================================================
|
||
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
|
||
// =============================================================================
|
||
|
||
let game = null; // Локальная игра (одиночный режим)
|
||
let ws = null; // WebSocket соединение
|
||
let currentPlayerId = null; // ID текущего игрока
|
||
let currentRoomId = null; // ID текущей комнаты
|
||
let isMultiplayer = false; // Режим игры
|
||
let settings = {}; // Настройки
|
||
let leaderboard = []; // Таблица лидеров
|
||
let soundEnabled = true; // Звук включён
|
||
let currentGamePhase = 'waiting'; // Текущая фаза игры для мультиплеера
|
||
let wsConnecting = false; // Флаг подключения WebSocket
|
||
let botPersonalities = null; // Персональности ботов (загружается из pokerAI)
|
||
|
||
// Выбранные опции
|
||
const selectedOptions = {
|
||
'bot-count': '1',
|
||
'ai-difficulty': '1',
|
||
'starting-stack': '1000',
|
||
'blinds': '5/10',
|
||
'max-players': '6'
|
||
};
|
||
|
||
// Звуки
|
||
const sounds = {
|
||
deal: null,
|
||
check: null,
|
||
call: null,
|
||
bet: null,
|
||
fold: null,
|
||
win: null,
|
||
chip: null,
|
||
message: null
|
||
};
|
||
|
||
// =============================================================================
|
||
// ИНИЦИАЛИЗАЦИЯ
|
||
// =============================================================================
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadSettings();
|
||
loadLeaderboard();
|
||
initSounds();
|
||
loadCardBackSettings();
|
||
|
||
// Загружаем персональности ботов
|
||
if (typeof pokerAI !== 'undefined' && pokerAI.personalities) {
|
||
botPersonalities = {};
|
||
pokerAI.personalities.forEach(p => {
|
||
botPersonalities[p.style] = p;
|
||
});
|
||
}
|
||
|
||
// Инициализируем авторизацию
|
||
if (typeof initAuth === 'function') {
|
||
initAuth();
|
||
} else {
|
||
// Если auth.js не загружен, показываем главное меню
|
||
showScreen('main-menu');
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Инициализация звуков
|
||
*/
|
||
function initSounds() {
|
||
// Простые звуки через Web Audio API
|
||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
|
||
const createBeep = (frequency, duration) => {
|
||
return () => {
|
||
if (!soundEnabled) return;
|
||
|
||
const oscillator = audioContext.createOscillator();
|
||
const gainNode = audioContext.createGain();
|
||
|
||
oscillator.connect(gainNode);
|
||
gainNode.connect(audioContext.destination);
|
||
|
||
oscillator.frequency.value = frequency;
|
||
oscillator.type = 'sine';
|
||
|
||
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);
|
||
|
||
oscillator.start(audioContext.currentTime);
|
||
oscillator.stop(audioContext.currentTime + duration);
|
||
};
|
||
};
|
||
|
||
sounds.deal = createBeep(400, 0.1);
|
||
sounds.check = createBeep(600, 0.1);
|
||
sounds.call = createBeep(500, 0.15);
|
||
sounds.bet = createBeep(700, 0.2);
|
||
sounds.fold = createBeep(300, 0.2);
|
||
sounds.win = createBeep(800, 0.4);
|
||
sounds.chip = createBeep(1000, 0.05);
|
||
sounds.message = createBeep(900, 0.1);
|
||
}
|
||
|
||
/**
|
||
* Воспроизвести звук
|
||
*/
|
||
function playSound(name) {
|
||
if (sounds[name] && soundEnabled) {
|
||
sounds[name]();
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// НАВИГАЦИЯ
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Показать экран
|
||
*/
|
||
function showScreen(screenId) {
|
||
document.querySelectorAll('.screen').forEach(screen => {
|
||
screen.classList.remove('active');
|
||
});
|
||
document.getElementById(screenId).classList.add('active');
|
||
|
||
// Особая логика для мультиплеера
|
||
if (screenId === 'multiplayer-menu') {
|
||
connectWebSocket();
|
||
}
|
||
|
||
// Загрузка лидерборда
|
||
if (screenId === 'leaderboard-screen') {
|
||
renderLeaderboard();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Переключение вкладок
|
||
*/
|
||
function switchTab(tabId) {
|
||
const parent = event.target.closest('.glass-container');
|
||
|
||
parent.querySelectorAll('.tab').forEach(tab => {
|
||
tab.classList.remove('active');
|
||
});
|
||
parent.querySelectorAll('.tab-content').forEach(content => {
|
||
content.classList.remove('active');
|
||
});
|
||
|
||
event.target.classList.add('active');
|
||
document.getElementById(tabId).classList.add('active');
|
||
}
|
||
|
||
/**
|
||
* Выбор опции
|
||
*/
|
||
function selectOption(button, optionGroup) {
|
||
const group = button.parentElement;
|
||
group.querySelectorAll('.btn-option').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
button.classList.add('active');
|
||
selectedOptions[optionGroup] = button.dataset.value;
|
||
}
|
||
|
||
// =============================================================================
|
||
// ОДИНОЧНАЯ ИГРА
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Начать одиночную игру
|
||
*/
|
||
function startSinglePlayer() {
|
||
isMultiplayer = false;
|
||
|
||
const playerName = document.getElementById('sp-player-name').value || 'Игрок';
|
||
const botCount = parseInt(selectedOptions['bot-count']);
|
||
const aiDifficulty = parseInt(selectedOptions['ai-difficulty']);
|
||
const startingStack = parseInt(selectedOptions['starting-stack']);
|
||
|
||
// Сохраняем имя
|
||
localStorage.setItem('playerName', playerName);
|
||
|
||
// Создаём игру
|
||
game = new PokerGame({
|
||
smallBlind: 5,
|
||
bigBlind: 10,
|
||
onUpdate: updateGameUI,
|
||
onAction: onPlayerAction,
|
||
onHandEnd: onHandEnd
|
||
});
|
||
|
||
// Добавляем игрока
|
||
const player = new Player('player_0', playerName, startingStack, false);
|
||
game.addPlayer(player);
|
||
currentPlayerId = player.id;
|
||
|
||
// Добавляем ботов с персональностями
|
||
const personalities = typeof pokerAI !== 'undefined' && pokerAI.personalities
|
||
? pokerAI.personalities
|
||
: [];
|
||
|
||
// Перемешиваем персональности для разнообразия
|
||
const shuffledPersonalities = personalities.length > 0
|
||
? [...personalities].sort(() => Math.random() - 0.5)
|
||
: [];
|
||
|
||
for (let i = 0; i < botCount; i++) {
|
||
let botName, personalityId, personality;
|
||
|
||
if (shuffledPersonalities.length > 0) {
|
||
// Используем персональность из списка
|
||
personality = shuffledPersonalities[i % shuffledPersonalities.length];
|
||
botName = personality.name;
|
||
personalityId = personality.style;
|
||
} else {
|
||
// Запасной вариант
|
||
botName = pokerAI.getRandomName();
|
||
personalityId = 'professional';
|
||
}
|
||
|
||
const bot = new Player(`bot_${i}`, botName, startingStack, true, aiDifficulty);
|
||
bot.personalityId = personalityId;
|
||
|
||
game.addPlayer(bot);
|
||
}
|
||
|
||
// Показываем игровой экран
|
||
showScreen('game-screen');
|
||
|
||
// Начинаем раздачу
|
||
setTimeout(() => {
|
||
game.startNewHand();
|
||
}, 500);
|
||
}
|
||
|
||
/**
|
||
* Новая раздача
|
||
*/
|
||
function startNewHand() {
|
||
document.getElementById('new-hand-btn').style.display = 'none';
|
||
|
||
if (isMultiplayer) {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({ type: 'new_hand' }));
|
||
}
|
||
} else {
|
||
if (game && game.getPlayersWithChips().length >= 2) {
|
||
game.startNewHand();
|
||
} else {
|
||
showNotification('Недостаточно игроков с фишками', 'error');
|
||
}
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// МУЛЬТИПЛЕЕР
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Подключение к WebSocket серверу
|
||
*/
|
||
function connectWebSocket() {
|
||
const serverUrl = settings.serverUrl || 'ws://localhost:3000';
|
||
|
||
// Если уже подключены или подключаемся
|
||
if ((ws && ws.readyState === WebSocket.OPEN) || wsConnecting) {
|
||
return Promise.resolve();
|
||
}
|
||
|
||
// Если соединение в процессе закрытия, ждём
|
||
if (ws && ws.readyState === WebSocket.CONNECTING) {
|
||
return new Promise((resolve) => {
|
||
ws.addEventListener('open', resolve, { once: true });
|
||
});
|
||
}
|
||
|
||
wsConnecting = true;
|
||
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
ws = new WebSocket(serverUrl);
|
||
|
||
ws.onopen = () => {
|
||
console.log('Подключено к серверу');
|
||
wsConnecting = false;
|
||
document.getElementById('room-list').innerHTML = '<div class="room-list-loading">Загрузка комнат...</div>';
|
||
resolve();
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
const message = JSON.parse(event.data);
|
||
handleServerMessage(message);
|
||
};
|
||
|
||
ws.onclose = () => {
|
||
console.log('Отключено от сервера');
|
||
wsConnecting = false;
|
||
ws = null;
|
||
document.getElementById('room-list').innerHTML = '<div class="room-list-loading">Соединение потеряно. <button class="btn btn-small" onclick="connectWebSocket()">Переподключиться</button></div>';
|
||
};
|
||
|
||
ws.onerror = (error) => {
|
||
console.error('WebSocket ошибка:', error);
|
||
wsConnecting = false;
|
||
document.getElementById('room-list').innerHTML = '<div class="room-list-loading">Ошибка подключения к серверу. <button class="btn btn-small" onclick="connectWebSocket()">Переподключиться</button></div>';
|
||
reject(error);
|
||
};
|
||
} catch (error) {
|
||
console.error('Ошибка создания WebSocket:', error);
|
||
wsConnecting = false;
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Обработка сообщений от сервера
|
||
*/
|
||
function handleServerMessage(message) {
|
||
switch (message.type) {
|
||
case 'room_list':
|
||
renderRoomList(message.rooms);
|
||
break;
|
||
|
||
case 'room_joined':
|
||
currentPlayerId = message.playerId;
|
||
currentRoomId = message.roomId;
|
||
showRoomLobby(message.room);
|
||
break;
|
||
|
||
case 'player_joined':
|
||
updateLobbyPlayers(message.room);
|
||
addChatMessage('lobby', null, `${message.player.name} присоединился`, true);
|
||
break;
|
||
|
||
case 'player_left':
|
||
case 'player_disconnected':
|
||
updateLobbyPlayers(message.room);
|
||
break;
|
||
|
||
case 'game_started':
|
||
isMultiplayer = true;
|
||
currentGamePhase = message.room.gamePhase;
|
||
showScreen('game-screen');
|
||
updateGameUIFromServer(message.room);
|
||
break;
|
||
|
||
case 'game_update':
|
||
currentGamePhase = message.room.gamePhase;
|
||
updateGameUIFromServer(message.room);
|
||
if (message.lastAction) {
|
||
playSound(message.lastAction.action);
|
||
}
|
||
break;
|
||
|
||
case 'chat':
|
||
if (document.getElementById('game-screen').classList.contains('active')) {
|
||
addChatMessage('game', message.playerName, message.message);
|
||
} else {
|
||
addChatMessage('lobby', message.playerName, message.message);
|
||
}
|
||
playSound('message');
|
||
break;
|
||
|
||
case 'error':
|
||
showNotification(message.message, 'error');
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отрисовка списка комнат
|
||
*/
|
||
function renderRoomList(rooms) {
|
||
const container = document.getElementById('room-list');
|
||
|
||
if (rooms.length === 0) {
|
||
container.innerHTML = '<div class="room-list-loading">Нет доступных комнат</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = rooms.map(room => `
|
||
<div class="room-item" onclick="joinRoom('${room.id}')">
|
||
<div class="room-info">
|
||
<h4>${room.name}</h4>
|
||
<span>Блайнды: ${room.smallBlind}/${room.bigBlind}</span>
|
||
</div>
|
||
<div class="room-players">
|
||
<span class="room-status ${room.isGameStarted ? 'playing' : ''}"></span>
|
||
${room.players}/${room.maxPlayers}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
/**
|
||
* Обновить список комнат
|
||
*/
|
||
function refreshRooms() {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({ type: 'get_rooms' }));
|
||
} else {
|
||
connectWebSocket();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Создать комнату
|
||
*/
|
||
async function createRoom() {
|
||
// Проверяем и устанавливаем соединение
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
try {
|
||
await connectWebSocket();
|
||
} catch (e) {
|
||
showNotification('Не удалось подключиться к серверу', 'error');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Повторная проверка после await
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
showNotification('Нет соединения с сервером', 'error');
|
||
return;
|
||
}
|
||
|
||
const playerName = document.getElementById('mp-player-name').value || 'Игрок';
|
||
const roomName = document.getElementById('room-name').value || `Комната ${playerName}`;
|
||
const [smallBlind, bigBlind] = selectedOptions['blinds'].split('/').map(Number);
|
||
const maxPlayers = parseInt(selectedOptions['max-players']);
|
||
|
||
localStorage.setItem('playerName', playerName);
|
||
|
||
ws.send(JSON.stringify({
|
||
type: 'create_room',
|
||
roomName,
|
||
playerName,
|
||
smallBlind,
|
||
bigBlind,
|
||
maxPlayers
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Присоединиться к комнате
|
||
*/
|
||
async function joinRoom(roomId) {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
try {
|
||
await connectWebSocket();
|
||
} catch (e) {
|
||
showNotification('Не удалось подключиться к серверу', 'error');
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
showNotification('Нет соединения с сервером', 'error');
|
||
return;
|
||
}
|
||
|
||
const playerName = document.getElementById('mp-player-name').value || 'Игрок';
|
||
localStorage.setItem('playerName', playerName);
|
||
|
||
ws.send(JSON.stringify({
|
||
type: 'join_room',
|
||
roomId,
|
||
playerName
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Показать лобби комнаты
|
||
*/
|
||
function showRoomLobby(room) {
|
||
showScreen('room-lobby');
|
||
|
||
document.getElementById('lobby-room-name').textContent = room.name;
|
||
document.getElementById('lobby-blinds').textContent = `${room.smallBlind}/${room.bigBlind}`;
|
||
|
||
updateLobbyPlayers(room);
|
||
}
|
||
|
||
/**
|
||
* Обновить игроков в лобби
|
||
*/
|
||
function updateLobbyPlayers(room) {
|
||
const container = document.getElementById('lobby-players');
|
||
|
||
container.innerHTML = room.players.map((player, index) => `
|
||
<div class="lobby-player ${index === 0 ? 'host' : ''}">
|
||
<div class="lobby-player-avatar">
|
||
${player.name.charAt(0).toUpperCase()}
|
||
</div>
|
||
<div class="lobby-player-name">${player.name}</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
document.getElementById('lobby-player-count').textContent =
|
||
`${room.players.length}/${room.maxPlayers || 6}`;
|
||
|
||
const startBtn = document.getElementById('start-game-btn');
|
||
startBtn.style.display = room.players[0]?.id === currentPlayerId ? 'block' : 'none';
|
||
startBtn.disabled = room.players.length < 2;
|
||
}
|
||
|
||
/**
|
||
* Выйти из комнаты
|
||
*/
|
||
function leaveRoom() {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({ type: 'leave_room' }));
|
||
}
|
||
currentRoomId = null;
|
||
showScreen('multiplayer-menu');
|
||
}
|
||
|
||
/**
|
||
* Начать мультиплеерную игру
|
||
*/
|
||
function startMultiplayerGame() {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({ type: 'start_game' }));
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// ИГРОВОЙ UI
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Обновить UI игры (локальный режим)
|
||
*/
|
||
function updateGameUI() {
|
||
if (!game) return;
|
||
|
||
document.getElementById('pot-amount').textContent = game.pot;
|
||
|
||
const phaseNames = {
|
||
'waiting': 'Ожидание',
|
||
'preflop': 'Префлоп',
|
||
'flop': 'Флоп',
|
||
'turn': 'Тёрн',
|
||
'river': 'Ривер',
|
||
'showdown': 'Вскрытие'
|
||
};
|
||
document.getElementById('game-phase').textContent = phaseNames[game.gamePhase] || game.gamePhase;
|
||
|
||
renderCommunityCards(game.communityCards);
|
||
renderPlayers(game.players, game.currentPlayerIndex, currentPlayerId);
|
||
|
||
const player = game.players.find(p => p.id === currentPlayerId);
|
||
if (player) {
|
||
renderPlayerHand(player.hand, game.communityCards);
|
||
}
|
||
|
||
updateActionPanel(player, game);
|
||
}
|
||
|
||
/**
|
||
* Обновить UI из данных сервера
|
||
*/
|
||
function updateGameUIFromServer(room) {
|
||
document.getElementById('pot-amount').textContent = room.pot;
|
||
|
||
const phaseNames = {
|
||
'waiting': 'Ожидание',
|
||
'preflop': 'Префлоп',
|
||
'flop': 'Флоп',
|
||
'turn': 'Тёрн',
|
||
'river': 'Ривер',
|
||
'showdown': 'Вскрытие'
|
||
};
|
||
document.getElementById('game-phase').textContent = phaseNames[room.gamePhase] || room.gamePhase;
|
||
|
||
// Конвертируем данные сервера в объекты Card
|
||
const communityCards = room.communityCards.map(c => new Card(c.suit, c.rank));
|
||
renderCommunityCards(communityCards);
|
||
|
||
// Конвертируем игроков
|
||
const players = room.players.map((p, i) => {
|
||
const player = {
|
||
...p,
|
||
hand: p.hand ? p.hand.map(c => new Card(c.suit, c.rank)) : []
|
||
};
|
||
return player;
|
||
});
|
||
|
||
renderPlayers(players, room.currentPlayerIndex, currentPlayerId);
|
||
|
||
const myPlayer = players.find(p => p.id === currentPlayerId);
|
||
if (myPlayer) {
|
||
renderPlayerHand(myPlayer.hand, communityCards);
|
||
updateActionPanelFromServer(myPlayer, room);
|
||
}
|
||
|
||
// Показываем кнопку новой раздачи если игра завершена
|
||
if (room.gamePhase === 'showdown' || !room.isGameStarted) {
|
||
setTimeout(() => {
|
||
document.getElementById('new-hand-btn').style.display = 'block';
|
||
}, 2000);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отрисовка общих карт
|
||
*/
|
||
function renderCommunityCards(cards) {
|
||
const container = document.getElementById('community-cards');
|
||
container.innerHTML = '';
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
if (cards[i]) {
|
||
const cardEl = cards[i].toHTML ? cards[i].toHTML() : createCardElement(cards[i]);
|
||
cardEl.classList.add('dealing');
|
||
cardEl.style.animationDelay = `${i * 0.1}s`;
|
||
container.appendChild(cardEl);
|
||
} else {
|
||
const placeholder = document.createElement('div');
|
||
placeholder.className = 'card card-placeholder';
|
||
placeholder.style.opacity = '0.2';
|
||
container.appendChild(placeholder);
|
||
}
|
||
}
|
||
|
||
playSound('deal');
|
||
}
|
||
|
||
/**
|
||
* Создать элемент карты из данных
|
||
*/
|
||
function createCardElement(cardData, isSmall = false) {
|
||
const card = document.createElement('div');
|
||
card.className = `card ${cardData.suit}${isSmall ? ' card-small' : ''}`;
|
||
|
||
const symbols = {
|
||
hearts: '♥',
|
||
diamonds: '♦',
|
||
clubs: '♣',
|
||
spades: '♠'
|
||
};
|
||
|
||
card.innerHTML = `
|
||
<span class="card-rank">${cardData.rank}</span>
|
||
<span class="card-suit">${symbols[cardData.suit]}</span>
|
||
`;
|
||
|
||
return card;
|
||
}
|
||
|
||
/**
|
||
* Отрисовка игроков за столом
|
||
*/
|
||
function renderPlayers(players, currentIndex, myPlayerId) {
|
||
const container = document.getElementById('player-positions');
|
||
container.innerHTML = '';
|
||
|
||
// Позиции для ставок (относительно позиции игрока)
|
||
const betPositions = [
|
||
{ top: '-40px', left: '50%' },
|
||
{ top: '-30px', left: '80%' },
|
||
{ top: '50%', left: '100%' },
|
||
{ top: '100%', left: '50%' },
|
||
{ top: '50%', left: '-30%' },
|
||
{ top: '-30px', left: '20%' }
|
||
];
|
||
|
||
players.forEach((player, index) => {
|
||
// Пропускаем текущего игрока (он отображается снизу)
|
||
if (player.id === myPlayerId) return;
|
||
|
||
const seat = document.createElement('div');
|
||
seat.className = 'player-seat';
|
||
seat.dataset.position = index;
|
||
|
||
const isCurrentTurn = index === currentIndex;
|
||
const playerBox = document.createElement('div');
|
||
playerBox.className = `player-box ${player.folded ? 'folded' : ''} ${isCurrentTurn ? 'current-turn' : ''}`;
|
||
|
||
playerBox.innerHTML = `
|
||
<div class="player-name">${player.name}</div>
|
||
<div class="player-chips">${player.chips}</div>
|
||
${player.lastAction ? `<div class="player-action">${player.lastAction}</div>` : ''}
|
||
`;
|
||
|
||
// Определяем, нужно ли показывать карты
|
||
// Показываем карты только на showdown и только не фолднувшим игрокам
|
||
const gamePhase = isMultiplayer ? currentGamePhase : (game ? game.gamePhase : 'waiting');
|
||
const showCards = gamePhase === 'showdown' && !player.folded;
|
||
|
||
// Карты игрока (мини)
|
||
if (showCards && player.hand && player.hand.length > 0) {
|
||
const cardsDiv = document.createElement('div');
|
||
cardsDiv.className = 'player-cards-mini';
|
||
|
||
player.hand.forEach(card => {
|
||
const cardEl = card.toHTML ? card.toHTML(true) : createCardElement(card, true);
|
||
cardsDiv.appendChild(cardEl);
|
||
});
|
||
|
||
playerBox.appendChild(cardsDiv);
|
||
} else if ((player.hasCards || (player.hand && player.hand.length > 0)) && !player.folded) {
|
||
// Карты рубашкой (не показываем карты соперников до showdown)
|
||
const cardsDiv = document.createElement('div');
|
||
cardsDiv.className = 'player-cards-mini';
|
||
cardsDiv.innerHTML = `
|
||
<div class="card card-small card-back"></div>
|
||
<div class="card card-small card-back"></div>
|
||
`;
|
||
playerBox.appendChild(cardsDiv);
|
||
}
|
||
|
||
seat.appendChild(playerBox);
|
||
|
||
// Позиционные маркеры
|
||
if (player.isDealer) {
|
||
const dealerBtn = document.createElement('div');
|
||
dealerBtn.className = 'dealer-button';
|
||
dealerBtn.textContent = 'D';
|
||
dealerBtn.style.cssText = 'bottom: -35px; left: 50%; transform: translateX(-50%);';
|
||
seat.appendChild(dealerBtn);
|
||
}
|
||
|
||
if (player.isSmallBlind || player.isBigBlind) {
|
||
const blindIndicator = document.createElement('div');
|
||
blindIndicator.className = `blind-indicator ${player.isSmallBlind ? 'sb' : 'bb'}`;
|
||
blindIndicator.textContent = player.isSmallBlind ? 'SB' : 'BB';
|
||
blindIndicator.style.cssText = 'bottom: -35px; right: 0;';
|
||
seat.appendChild(blindIndicator);
|
||
}
|
||
|
||
// Ставка игрока
|
||
if (player.bet > 0) {
|
||
const betDisplay = document.createElement('div');
|
||
betDisplay.className = 'player-bet-display';
|
||
betDisplay.textContent = player.bet;
|
||
betDisplay.style.cssText = `${betPositions[index % 6].top}; left: ${betPositions[index % 6].left};`;
|
||
seat.appendChild(betDisplay);
|
||
}
|
||
|
||
container.appendChild(seat);
|
||
});
|
||
|
||
// Применяем стиль рубашки к новым картам
|
||
applyCardBackToNewCards();
|
||
}
|
||
|
||
/**
|
||
* Применить стиль рубашки к новым картам на столе
|
||
*/
|
||
function applyCardBackToNewCards() {
|
||
const style = settings.cardBackStyle || 'default';
|
||
|
||
if (style === 'custom') {
|
||
const url = localStorage.getItem('customCardBack');
|
||
if (url) {
|
||
document.querySelectorAll('.card-back:not(.styled)').forEach(card => {
|
||
card.classList.add('style-custom', 'styled');
|
||
card.style.backgroundImage = `url(${url})`;
|
||
card.style.backgroundSize = 'cover';
|
||
card.style.backgroundPosition = 'center';
|
||
});
|
||
}
|
||
} else {
|
||
document.querySelectorAll('.card-back:not(.styled)').forEach(card => {
|
||
card.classList.add(`style-${style}`, 'styled');
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отрисовка руки игрока
|
||
*/
|
||
function renderPlayerHand(hand, communityCards) {
|
||
const container = document.getElementById('player-cards');
|
||
container.innerHTML = '';
|
||
|
||
if (!hand || hand.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="card card-back"></div>
|
||
<div class="card card-back"></div>
|
||
`;
|
||
applyCardBackToNewCards();
|
||
return;
|
||
}
|
||
|
||
hand.forEach((card, i) => {
|
||
const cardEl = card.toHTML ? card.toHTML() : createCardElement(card);
|
||
cardEl.classList.add('dealing');
|
||
cardEl.style.animationDelay = `${i * 0.15}s`;
|
||
container.appendChild(cardEl);
|
||
});
|
||
|
||
// Показываем силу руки
|
||
if (settings.showHandStrength !== false) {
|
||
const strength = getHandStrength(hand, communityCards || []);
|
||
document.getElementById('hand-strength').textContent = strength || '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обновить панель действий
|
||
*/
|
||
function updateActionPanel(player, gameState) {
|
||
if (!player || !gameState || !gameState.isGameStarted) {
|
||
document.getElementById('action-panel').style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const isMyTurn = gameState.getCurrentPlayer()?.id === player.id;
|
||
document.getElementById('action-panel').style.display = isMyTurn ? 'block' : 'none';
|
||
|
||
if (!isMyTurn) return;
|
||
|
||
const toCall = gameState.currentBet - player.bet;
|
||
const canCheck = toCall === 0;
|
||
const canBet = gameState.currentBet === 0;
|
||
|
||
document.getElementById('btn-check').style.display = canCheck ? 'inline-flex' : 'none';
|
||
document.getElementById('btn-call').style.display = !canCheck ? 'inline-flex' : 'none';
|
||
document.getElementById('btn-bet').style.display = canBet ? 'inline-flex' : 'none';
|
||
document.getElementById('btn-raise').style.display = !canBet ? 'inline-flex' : 'none';
|
||
|
||
document.getElementById('call-amount').textContent = toCall;
|
||
|
||
// Настройка слайдера
|
||
const slider = document.getElementById('bet-slider');
|
||
const minBet = canBet ? gameState.bigBlind : gameState.currentBet + gameState.lastRaiseAmount;
|
||
slider.min = minBet;
|
||
slider.max = player.chips + player.bet;
|
||
slider.value = minBet;
|
||
document.getElementById('bet-value').value = minBet;
|
||
}
|
||
|
||
/**
|
||
* Обновить панель действий из данных сервера
|
||
*/
|
||
function updateActionPanelFromServer(player, room) {
|
||
if (!player || room.gamePhase === 'showdown' || !room.isGameStarted) {
|
||
document.getElementById('action-panel').style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const isMyTurn = room.currentPlayerId === player.id;
|
||
document.getElementById('action-panel').style.display = isMyTurn ? 'block' : 'none';
|
||
|
||
if (!isMyTurn) return;
|
||
|
||
const toCall = room.currentBet - player.bet;
|
||
const canCheck = toCall === 0;
|
||
const canBet = room.currentBet === 0;
|
||
|
||
document.getElementById('btn-check').style.display = canCheck ? 'inline-flex' : 'none';
|
||
document.getElementById('btn-call').style.display = !canCheck ? 'inline-flex' : 'none';
|
||
document.getElementById('btn-bet').style.display = canBet ? 'inline-flex' : 'none';
|
||
document.getElementById('btn-raise').style.display = !canBet ? 'inline-flex' : 'none';
|
||
|
||
document.getElementById('call-amount').textContent = toCall;
|
||
|
||
const slider = document.getElementById('bet-slider');
|
||
const bigBlind = room.bigBlind || 10;
|
||
const minBet = canBet ? bigBlind : room.minRaise || room.currentBet * 2;
|
||
slider.min = minBet;
|
||
slider.max = player.chips + player.bet;
|
||
slider.value = minBet;
|
||
document.getElementById('bet-value').value = minBet;
|
||
}
|
||
|
||
// =============================================================================
|
||
// ДЕЙСТВИЯ ИГРОКА
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Выполнить действие
|
||
*/
|
||
function playerAction(action) {
|
||
playSound(action);
|
||
|
||
if (isMultiplayer) {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({
|
||
type: 'action',
|
||
action: action,
|
||
amount: 0
|
||
}));
|
||
}
|
||
} else {
|
||
if (game) {
|
||
game.processAction(currentPlayerId, action, 0);
|
||
}
|
||
}
|
||
|
||
hideBetSlider();
|
||
}
|
||
|
||
/**
|
||
* Показать слайдер ставки
|
||
*/
|
||
function showBetSlider() {
|
||
document.getElementById('bet-slider-container').style.display = 'block';
|
||
}
|
||
|
||
/**
|
||
* Скрыть слайдер ставки
|
||
*/
|
||
function hideBetSlider() {
|
||
document.getElementById('bet-slider-container').style.display = 'none';
|
||
}
|
||
|
||
/**
|
||
* Обновить значение ставки из слайдера
|
||
*/
|
||
function updateBetValue() {
|
||
const slider = document.getElementById('bet-slider');
|
||
document.getElementById('bet-value').value = slider.value;
|
||
}
|
||
|
||
/**
|
||
* Обновить слайдер из инпута
|
||
*/
|
||
function updateSliderFromInput() {
|
||
const input = document.getElementById('bet-value');
|
||
const slider = document.getElementById('bet-slider');
|
||
|
||
let value = parseInt(input.value);
|
||
value = Math.max(parseInt(slider.min), Math.min(parseInt(slider.max), value));
|
||
|
||
slider.value = value;
|
||
input.value = value;
|
||
}
|
||
|
||
/**
|
||
* Установить пресет ставки
|
||
*/
|
||
function setBetPreset(multiplier) {
|
||
let pot;
|
||
|
||
if (isMultiplayer) {
|
||
pot = parseInt(document.getElementById('pot-amount').textContent) || 0;
|
||
} else {
|
||
pot = game ? game.pot : 0;
|
||
}
|
||
|
||
const betAmount = Math.floor(pot * multiplier);
|
||
const slider = document.getElementById('bet-slider');
|
||
const value = Math.max(parseInt(slider.min), Math.min(parseInt(slider.max), betAmount));
|
||
|
||
slider.value = value;
|
||
document.getElementById('bet-value').value = value;
|
||
}
|
||
|
||
/**
|
||
* Подтвердить ставку
|
||
*/
|
||
function confirmBet() {
|
||
const amount = parseInt(document.getElementById('bet-value').value);
|
||
const action = document.getElementById('btn-bet').style.display !== 'none' ? 'bet' : 'raise';
|
||
|
||
playSound('bet');
|
||
|
||
if (isMultiplayer) {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({
|
||
type: 'action',
|
||
action: action,
|
||
amount: amount
|
||
}));
|
||
}
|
||
} else {
|
||
if (game) {
|
||
game.processAction(currentPlayerId, action, amount);
|
||
}
|
||
}
|
||
|
||
hideBetSlider();
|
||
}
|
||
|
||
// =============================================================================
|
||
// СОБЫТИЯ ИГРЫ
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Событие действия игрока
|
||
*/
|
||
function onPlayerAction(player, action, amount) {
|
||
// Можно добавить анимации или уведомления
|
||
console.log(`${player.name}: ${action} ${amount || ''}`);
|
||
}
|
||
|
||
/**
|
||
* Событие завершения раздачи
|
||
*/
|
||
function onHandEnd(result) {
|
||
playSound('win');
|
||
|
||
// Показываем результат
|
||
const modal = document.getElementById('hand-result-modal');
|
||
const title = document.getElementById('result-title');
|
||
const details = document.getElementById('result-details');
|
||
|
||
const winner = result.winners[0];
|
||
|
||
title.textContent = result.winners.length > 1 ? 'Сплит!' : `${winner.name} победил!`;
|
||
|
||
let detailsHTML = `
|
||
<div class="result-amount">+${winner.amount}</div>
|
||
`;
|
||
|
||
if (winner.hand) {
|
||
detailsHTML += `<div class="result-hand">${winner.hand.name}</div>`;
|
||
}
|
||
|
||
if (result.hands && result.hands.length > 1) {
|
||
detailsHTML += '<div class="all-hands">';
|
||
result.hands.forEach(h => {
|
||
detailsHTML += `<div>${h.player.name}: ${h.hand.name}</div>`;
|
||
});
|
||
detailsHTML += '</div>';
|
||
}
|
||
|
||
details.innerHTML = detailsHTML;
|
||
modal.classList.add('active');
|
||
|
||
// Обновляем лидерборд
|
||
updateLeaderboard(result);
|
||
|
||
// НОВОЕ: Генерируем эмоциональные реакции ботов
|
||
if (!isMultiplayer && game) {
|
||
generateBotEmotions(result);
|
||
}
|
||
|
||
// Показываем кнопку новой раздачи
|
||
setTimeout(() => {
|
||
document.getElementById('new-hand-btn').style.display = 'block';
|
||
}, 1000);
|
||
}
|
||
|
||
/**
|
||
* Генерировать эмоциональные реакции ботов после раздачи
|
||
*/
|
||
async function generateBotEmotions(result) {
|
||
if (!game || !result) return;
|
||
|
||
const winnerIds = result.winners.map(w => w.id);
|
||
const potSize = result.pot || game.pot || 0;
|
||
|
||
// Определяем, был ли олл-ин
|
||
const wasAllIn = game.players.some(p => p.chips === 0 && !p.folded);
|
||
|
||
// Случайно решаем, будет ли бот реагировать (30-50% вероятность)
|
||
const shouldReact = Math.random() < (wasAllIn ? 0.5 : 0.3);
|
||
|
||
if (!shouldReact) return;
|
||
|
||
// Выбираем случайного бота для реакции
|
||
const activeBots = game.players.filter(p => p.isAI && !p.folded);
|
||
if (activeBots.length === 0) return;
|
||
|
||
const bot = activeBots[Math.floor(Math.random() * activeBots.length)];
|
||
const isWinner = winnerIds.includes(bot.id);
|
||
|
||
// Получаем личность бота
|
||
let botPersonality;
|
||
if (typeof botPersonalities !== 'undefined' && bot.personalityId) {
|
||
botPersonality = botPersonalities[bot.personalityId];
|
||
}
|
||
|
||
if (!botPersonality) {
|
||
botPersonality = { style: 'professional' };
|
||
}
|
||
|
||
// Задержка перед реакцией (500-1500ms)
|
||
setTimeout(async () => {
|
||
try {
|
||
let reaction;
|
||
|
||
if (typeof llmChat !== 'undefined' && llmChat.generateEmotionalReaction) {
|
||
reaction = await llmChat.generateEmotionalReaction(
|
||
bot,
|
||
botPersonality,
|
||
isWinner,
|
||
potSize,
|
||
wasAllIn,
|
||
{ phase: currentGamePhase }
|
||
);
|
||
} else {
|
||
// Запасной вариант
|
||
const emotions = isWinner
|
||
? ['Отлично!', 'Неплохо!', 'GG', 'Да!']
|
||
: ['Эх...', 'Невезение', 'Бывает', 'Хм'];
|
||
reaction = emotions[Math.floor(Math.random() * emotions.length)];
|
||
}
|
||
|
||
if (reaction) {
|
||
addChatMessage('game', bot.name, reaction);
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка генерации эмоции:', error);
|
||
}
|
||
}, 500 + Math.random() * 1000);
|
||
}
|
||
|
||
/**
|
||
* Закрыть модальное окно результата
|
||
*/
|
||
function closeResultModal() {
|
||
document.getElementById('hand-result-modal').classList.remove('active');
|
||
}
|
||
|
||
// =============================================================================
|
||
// ЧАТ
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Добавить сообщение в чат
|
||
*/
|
||
function addChatMessage(chatType, sender, message, isSystem = false, isTyping = false) {
|
||
const containerId = chatType === 'game' ? 'game-chat-messages' : 'lobby-chat-messages';
|
||
const container = document.getElementById(containerId);
|
||
|
||
const msgDiv = document.createElement('div');
|
||
msgDiv.className = `chat-message ${isSystem ? 'system' : ''} ${isTyping ? 'typing-indicator' : ''}`;
|
||
|
||
if (isTyping) {
|
||
msgDiv.dataset.sender = sender;
|
||
}
|
||
|
||
if (isSystem) {
|
||
msgDiv.textContent = message;
|
||
} else {
|
||
const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||
if (isTyping) {
|
||
msgDiv.innerHTML = `
|
||
<span class="sender">${sender}</span>: <span class="typing-dots">печатает<span>.</span><span>.</span><span>.</span></span>
|
||
<span class="time">${time}</span>
|
||
`;
|
||
} else {
|
||
msgDiv.innerHTML = `
|
||
<span class="sender">${sender}</span>: ${message}
|
||
<span class="time">${time}</span>
|
||
`;
|
||
}
|
||
}
|
||
|
||
container.appendChild(msgDiv);
|
||
container.scrollTop = container.scrollHeight;
|
||
}
|
||
|
||
/**
|
||
* Удалить индикатор "печатает..." для конкретного отправителя
|
||
*/
|
||
function removeTypingIndicator(sender, chatType = 'game') {
|
||
const containerId = chatType === 'game' ? 'game-chat-messages' : 'lobby-chat-messages';
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
// Используем querySelectorAll и проверяем dataset, чтобы избежать проблем с экранированием кавычек
|
||
const typingIndicators = container.querySelectorAll('.typing-indicator');
|
||
typingIndicators.forEach(indicator => {
|
||
if (indicator.dataset.sender === sender) {
|
||
indicator.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Отправить сообщение в чат лобби
|
||
*/
|
||
function sendLobbyChat() {
|
||
const input = document.getElementById('lobby-chat-input');
|
||
const message = input.value.trim();
|
||
|
||
if (!message) return;
|
||
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({
|
||
type: 'chat',
|
||
message: message
|
||
}));
|
||
}
|
||
|
||
input.value = '';
|
||
}
|
||
|
||
/**
|
||
* Отправить сообщение в игровой чат
|
||
*/
|
||
function sendGameChat() {
|
||
const input = document.getElementById('game-chat-input');
|
||
const message = input.value.trim();
|
||
|
||
if (!message) return;
|
||
|
||
if (isMultiplayer && ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({
|
||
type: 'chat',
|
||
message: message
|
||
}));
|
||
} else {
|
||
// Одиночная игра - ИИ отвечает
|
||
const playerName = localStorage.getItem('playerName') || 'Игрок';
|
||
addChatMessage('game', playerName, message);
|
||
|
||
// Получаем всех ботов для ответа
|
||
const bots = game.players.filter(p => p.isAI && !p.folded);
|
||
|
||
if (bots.length > 0) {
|
||
// Проверяем, обращается ли игрок к конкретному боту
|
||
let targetBot = null;
|
||
if (typeof llmChat !== 'undefined' && llmChat.detectBotMention) {
|
||
targetBot = llmChat.detectBotMention(message, bots);
|
||
}
|
||
|
||
// Если обращение не найдено, выбираем случайного бота
|
||
const bot = targetBot || bots[Math.floor(Math.random() * bots.length)];
|
||
|
||
// Формируем контекст игры для LLM
|
||
const gameContext = {
|
||
phase: currentGamePhase,
|
||
pot: game.pot,
|
||
myChips: bot.chips,
|
||
lastAction: message,
|
||
communityCards: game.communityCards?.map(c => `${c.rank}${c.suit}`) || [],
|
||
mentionedByName: targetBot !== null // Указываем, что игрок обратился по имени
|
||
};
|
||
|
||
// Получаем личность бота
|
||
const botPersonality = typeof botPersonalities !== 'undefined'
|
||
? botPersonalities[bot.personalityId] || botPersonalities.professional
|
||
: { style: 'default' };
|
||
|
||
// Показываем индикатор "печатает..."
|
||
addChatMessage('game', bot.name, '...', false, true);
|
||
|
||
// Вызываем LLM чат
|
||
(async () => {
|
||
try {
|
||
let response;
|
||
|
||
if (typeof llmChat !== 'undefined' && llmChat.getSettings().llmEnabled) {
|
||
response = await llmChat.chat(bot.id, botPersonality, message, gameContext);
|
||
} else {
|
||
// Запасные ответы если LLM отключён
|
||
await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 1500));
|
||
response = llmChat?.getFallbackResponse(botPersonality, message, gameContext) || 'Удачи!';
|
||
}
|
||
|
||
// Удаляем индикатор "печатает..."
|
||
removeTypingIndicator(bot.name);
|
||
|
||
// Добавляем ответ
|
||
addChatMessage('game', bot.name, response);
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка LLM чата:', error);
|
||
removeTypingIndicator(bot.name);
|
||
addChatMessage('game', bot.name, 'Удачи!');
|
||
}
|
||
})();
|
||
}
|
||
}
|
||
|
||
input.value = '';
|
||
}
|
||
|
||
/**
|
||
* Обработка Enter в чате лобби
|
||
*/
|
||
function handleLobbyChatKey(event) {
|
||
if (event.key === 'Enter') {
|
||
sendLobbyChat();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обработка Enter в игровом чате
|
||
*/
|
||
function handleGameChatKey(event) {
|
||
if (event.key === 'Enter') {
|
||
sendGameChat();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Переключить чат
|
||
*/
|
||
function toggleChat() {
|
||
document.getElementById('game-chat').classList.toggle('expanded');
|
||
}
|
||
|
||
// =============================================================================
|
||
// НАСТРОЙКИ
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Загрузить настройки
|
||
*/
|
||
function loadSettings() {
|
||
const saved = localStorage.getItem('pokerSettings');
|
||
if (saved) {
|
||
settings = JSON.parse(saved);
|
||
} else {
|
||
settings = {
|
||
sound: true,
|
||
animations: true,
|
||
showHandStrength: true,
|
||
autofold: true,
|
||
serverUrl: 'ws://localhost:3000',
|
||
llmEnabled: false,
|
||
llmProvider: 'ollama',
|
||
llmApiUrl: 'http://localhost:11434',
|
||
llmModel: 'llama3.2',
|
||
llmApiKey: ''
|
||
};
|
||
}
|
||
|
||
// Применяем настройки к UI
|
||
document.getElementById('setting-sound').checked = settings.sound !== false;
|
||
document.getElementById('setting-animations').checked = settings.animations !== false;
|
||
document.getElementById('setting-hand-strength').checked = settings.showHandStrength !== false;
|
||
document.getElementById('setting-autofold').checked = settings.autofold !== false;
|
||
document.getElementById('server-url').value = settings.serverUrl || 'ws://localhost:3000';
|
||
|
||
// LLM настройки
|
||
const llmEnabled = document.getElementById('setting-llm-enabled');
|
||
const llmProvider = document.getElementById('llm-provider');
|
||
const llmApiUrl = document.getElementById('llm-api-url');
|
||
const llmModel = document.getElementById('llm-model');
|
||
const llmApiKey = document.getElementById('llm-api-key');
|
||
|
||
if (llmEnabled) llmEnabled.checked = settings.llmEnabled || false;
|
||
if (llmProvider) llmProvider.value = settings.llmProvider || 'ollama';
|
||
if (llmApiUrl) llmApiUrl.value = settings.llmApiUrl || 'http://localhost:11434';
|
||
if (llmModel) llmModel.value = settings.llmModel || 'llama3.2';
|
||
if (llmApiKey) llmApiKey.value = settings.llmApiKey || '';
|
||
|
||
// Обновляем видимость API ключа
|
||
updateLLMProviderUI();
|
||
|
||
soundEnabled = settings.sound !== false;
|
||
}
|
||
|
||
/**
|
||
* Обновить UI для LLM провайдера
|
||
*/
|
||
function updateLLMProviderUI() {
|
||
const provider = document.getElementById('llm-provider')?.value || 'ollama';
|
||
const apiKeyGroup = document.getElementById('llm-api-key-group');
|
||
const apiUrlLabel = document.getElementById('llm-api-url-label');
|
||
const apiUrl = document.getElementById('llm-api-url');
|
||
|
||
if (apiKeyGroup) {
|
||
apiKeyGroup.style.display = (provider === 'openai') ? 'block' : 'none';
|
||
}
|
||
|
||
if (apiUrlLabel && apiUrl) {
|
||
switch (provider) {
|
||
case 'ollama':
|
||
apiUrl.placeholder = 'http://localhost:11434';
|
||
if (!apiUrl.value || apiUrl.value.includes('localhost:1234')) {
|
||
apiUrl.value = 'http://localhost:11434';
|
||
}
|
||
break;
|
||
case 'lmstudio':
|
||
apiUrl.placeholder = 'http://localhost:1234';
|
||
if (!apiUrl.value || apiUrl.value.includes('localhost:11434')) {
|
||
apiUrl.value = 'http://localhost:1234';
|
||
}
|
||
break;
|
||
case 'openai':
|
||
apiUrl.placeholder = 'https://api.openai.com';
|
||
apiUrl.value = 'https://api.openai.com';
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обновить настройки
|
||
*/
|
||
function updateSettings() {
|
||
settings = {
|
||
sound: document.getElementById('setting-sound').checked,
|
||
animations: document.getElementById('setting-animations').checked,
|
||
showHandStrength: document.getElementById('setting-hand-strength').checked,
|
||
autofold: document.getElementById('setting-autofold').checked,
|
||
serverUrl: document.getElementById('server-url').value,
|
||
llmEnabled: document.getElementById('setting-llm-enabled')?.checked || false,
|
||
llmProvider: document.getElementById('llm-provider')?.value || 'ollama',
|
||
llmApiUrl: document.getElementById('llm-api-url')?.value || 'http://localhost:11434',
|
||
llmModel: document.getElementById('llm-model')?.value || 'llama3.2',
|
||
llmApiKey: document.getElementById('llm-api-key')?.value || ''
|
||
};
|
||
|
||
localStorage.setItem('pokerSettings', JSON.stringify(settings));
|
||
soundEnabled = settings.sound;
|
||
}
|
||
|
||
/**
|
||
* Тестировать подключение к LLM
|
||
*/
|
||
async function testLLMConnection() {
|
||
const testBtn = document.getElementById('test-llm-btn');
|
||
const originalText = testBtn?.textContent;
|
||
|
||
if (testBtn) {
|
||
testBtn.textContent = 'Тестирую...';
|
||
testBtn.disabled = true;
|
||
}
|
||
|
||
// Сохраняем настройки перед тестом
|
||
updateSettings();
|
||
|
||
try {
|
||
if (typeof llmChat !== 'undefined') {
|
||
const result = await llmChat.testConnection();
|
||
|
||
if (result.success) {
|
||
showNotification('LLM подключён успешно! ' + (result.response?.substring(0, 50) || ''), 'success');
|
||
} else {
|
||
showNotification('Ошибка: ' + result.error, 'error');
|
||
}
|
||
} else {
|
||
showNotification('LLM модуль не загружен', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка: ' + error.message, 'error');
|
||
}
|
||
|
||
if (testBtn) {
|
||
testBtn.textContent = originalText;
|
||
testBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Выбрать LLM провайдера
|
||
*/
|
||
function selectLLMProvider(btn) {
|
||
// Убираем active со всех кнопок
|
||
const buttons = btn.parentElement.querySelectorAll('.btn-option');
|
||
buttons.forEach(b => b.classList.remove('active'));
|
||
|
||
// Добавляем active на выбранную
|
||
btn.classList.add('active');
|
||
|
||
// Устанавливаем значение в скрытый input
|
||
const value = btn.dataset.value;
|
||
document.getElementById('llm-provider').value = value;
|
||
|
||
// Обновляем UI
|
||
updateLLMProviderUI();
|
||
|
||
// Сохраняем настройки
|
||
updateSettings();
|
||
}
|
||
|
||
/**
|
||
* Сбросить настройки
|
||
*/
|
||
function resetSettings() {
|
||
localStorage.removeItem('pokerSettings');
|
||
localStorage.removeItem('customCardBack');
|
||
loadSettings();
|
||
applyCardBackStyle('default');
|
||
showNotification('Настройки сброшены', 'success');
|
||
}
|
||
|
||
// =============================================================================
|
||
// РУБАШКА КАРТ
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Выбрать стиль рубашки карт
|
||
*/
|
||
function selectCardBack(btn) {
|
||
// Убираем active со всех кнопок
|
||
const buttons = btn.parentElement.querySelectorAll('.btn-option');
|
||
buttons.forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
|
||
const style = btn.dataset.style;
|
||
|
||
// Показываем/скрываем поля для кастомной рубашки
|
||
const customGroup = document.getElementById('custom-card-back-group');
|
||
const urlGroup = document.getElementById('card-back-url-group');
|
||
|
||
if (style === 'custom') {
|
||
customGroup.style.display = 'block';
|
||
urlGroup.style.display = 'block';
|
||
} else {
|
||
customGroup.style.display = 'none';
|
||
urlGroup.style.display = 'none';
|
||
}
|
||
|
||
// Применяем стиль
|
||
applyCardBackStyle(style);
|
||
|
||
// Сохраняем в настройки
|
||
settings.cardBackStyle = style;
|
||
localStorage.setItem('pokerSettings', JSON.stringify(settings));
|
||
}
|
||
|
||
/**
|
||
* Загрузить кастомное изображение рубашки
|
||
*/
|
||
function loadCustomCardBack(input) {
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
const imageUrl = e.target.result;
|
||
setCustomCardBack(imageUrl);
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
/**
|
||
* Установить URL рубашки
|
||
*/
|
||
function setCardBackUrl(url) {
|
||
if (!url) return;
|
||
setCustomCardBack(url);
|
||
}
|
||
|
||
/**
|
||
* Установить кастомную рубашку
|
||
*/
|
||
function setCustomCardBack(imageUrl) {
|
||
// Сохраняем в localStorage
|
||
localStorage.setItem('customCardBack', imageUrl);
|
||
|
||
// Применяем стиль
|
||
applyCardBackStyle('custom', imageUrl);
|
||
|
||
showNotification('Рубашка карт обновлена!', 'success');
|
||
}
|
||
|
||
/**
|
||
* Применить стиль рубашки ко всем картам
|
||
*/
|
||
function applyCardBackStyle(style, customUrl = null) {
|
||
const root = document.documentElement;
|
||
const preview = document.getElementById('card-back-preview');
|
||
|
||
// Убираем все стили
|
||
document.querySelectorAll('.card-back').forEach(card => {
|
||
card.classList.remove('style-default', 'style-red', 'style-blue', 'style-custom');
|
||
card.style.backgroundImage = '';
|
||
});
|
||
|
||
if (style === 'custom') {
|
||
const url = customUrl || localStorage.getItem('customCardBack');
|
||
if (url) {
|
||
root.style.setProperty('--card-back-custom-url', `url(${url})`);
|
||
|
||
document.querySelectorAll('.card-back').forEach(card => {
|
||
card.classList.add('style-custom');
|
||
card.style.backgroundImage = `url(${url})`;
|
||
card.style.backgroundSize = 'cover';
|
||
card.style.backgroundPosition = 'center';
|
||
});
|
||
|
||
if (preview) {
|
||
preview.classList.add('style-custom');
|
||
preview.style.backgroundImage = `url(${url})`;
|
||
preview.style.backgroundSize = 'cover';
|
||
preview.style.backgroundPosition = 'center';
|
||
}
|
||
}
|
||
} else {
|
||
document.querySelectorAll('.card-back').forEach(card => {
|
||
card.classList.add(`style-${style}`);
|
||
});
|
||
|
||
if (preview) {
|
||
preview.className = 'card card-back';
|
||
preview.classList.add(`style-${style}`);
|
||
preview.style.backgroundImage = '';
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Загрузить настройки рубашки при старте
|
||
*/
|
||
function loadCardBackSettings() {
|
||
const style = settings.cardBackStyle || 'default';
|
||
|
||
// Активируем нужную кнопку
|
||
const buttons = document.querySelectorAll('.card-back-styles .btn-option');
|
||
buttons.forEach(btn => {
|
||
btn.classList.remove('active');
|
||
if (btn.dataset.style === style) {
|
||
btn.classList.add('active');
|
||
}
|
||
});
|
||
|
||
// Показываем поля для кастомной рубашки если нужно
|
||
const customGroup = document.getElementById('custom-card-back-group');
|
||
const urlGroup = document.getElementById('card-back-url-group');
|
||
|
||
if (customGroup && urlGroup) {
|
||
if (style === 'custom') {
|
||
customGroup.style.display = 'block';
|
||
urlGroup.style.display = 'block';
|
||
} else {
|
||
customGroup.style.display = 'none';
|
||
urlGroup.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Применяем стиль
|
||
applyCardBackStyle(style);
|
||
}
|
||
|
||
/**
|
||
* Переключить звук
|
||
*/
|
||
function toggleSound() {
|
||
soundEnabled = !soundEnabled;
|
||
settings.sound = soundEnabled;
|
||
localStorage.setItem('pokerSettings', JSON.stringify(settings));
|
||
|
||
const btn = document.getElementById('sound-toggle');
|
||
btn.textContent = soundEnabled ? '🔊' : '🔇';
|
||
|
||
showNotification(soundEnabled ? 'Звук включён' : 'Звук выключён', 'info');
|
||
}
|
||
|
||
// =============================================================================
|
||
// ЛИДЕРБОРД
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Загрузить лидерборд
|
||
*/
|
||
function loadLeaderboard() {
|
||
const saved = localStorage.getItem('pokerLeaderboard');
|
||
if (saved) {
|
||
leaderboard = JSON.parse(saved);
|
||
} else {
|
||
leaderboard = [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обновить лидерборд
|
||
*/
|
||
function updateLeaderboard(result) {
|
||
const playerName = localStorage.getItem('playerName') || 'Игрок';
|
||
|
||
// Находим или создаём запись
|
||
let entry = leaderboard.find(e => e.name === playerName);
|
||
if (!entry) {
|
||
entry = {
|
||
name: playerName,
|
||
gamesPlayed: 0,
|
||
handsWon: 0,
|
||
totalWinnings: 0,
|
||
biggestPot: 0
|
||
};
|
||
leaderboard.push(entry);
|
||
}
|
||
|
||
entry.gamesPlayed++;
|
||
|
||
// Проверяем, выиграл ли текущий игрок
|
||
const won = result.winners.some(w => w.id === currentPlayerId);
|
||
if (won) {
|
||
entry.handsWon++;
|
||
const winAmount = result.winners.find(w => w.id === currentPlayerId)?.amount || 0;
|
||
entry.totalWinnings += winAmount;
|
||
if (result.pot > entry.biggestPot) {
|
||
entry.biggestPot = result.pot;
|
||
}
|
||
}
|
||
|
||
// Сортируем и сохраняем
|
||
leaderboard.sort((a, b) => b.totalWinnings - a.totalWinnings);
|
||
localStorage.setItem('pokerLeaderboard', JSON.stringify(leaderboard));
|
||
}
|
||
|
||
/**
|
||
* Отрисовка лидерборда
|
||
*/
|
||
function renderLeaderboard() {
|
||
const container = document.getElementById('leaderboard-list');
|
||
|
||
if (leaderboard.length === 0) {
|
||
container.innerHTML = '<div class="room-list-loading">Нет записей</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = leaderboard.slice(0, 10).map((entry, index) => {
|
||
const rankClass = index === 0 ? 'gold' : index === 1 ? 'silver' : index === 2 ? 'bronze' : '';
|
||
|
||
return `
|
||
<div class="leaderboard-item">
|
||
<div class="leaderboard-rank ${rankClass}">${index + 1}</div>
|
||
<div class="leaderboard-info">
|
||
<div class="leaderboard-name">${entry.name}</div>
|
||
<div class="leaderboard-stats">
|
||
Игр: ${entry.gamesPlayed} | Побед: ${entry.handsWon} | Макс. банк: ${entry.biggestPot}
|
||
</div>
|
||
</div>
|
||
<div class="leaderboard-score">${entry.totalWinnings}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
/**
|
||
* Переключить вкладку лидерборда
|
||
*/
|
||
function switchLeaderboardTab(tab) {
|
||
const parent = document.querySelector('.leaderboard-tabs');
|
||
parent.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
event.target.classList.add('active');
|
||
|
||
if (tab === 'global') {
|
||
document.getElementById('leaderboard-list').innerHTML =
|
||
'<div class="room-list-loading">Глобальный лидерборд недоступен (требуется backend)</div>';
|
||
} else {
|
||
renderLeaderboard();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Очистить лидерборд
|
||
*/
|
||
function clearLeaderboard() {
|
||
if (confirm('Вы уверены, что хотите очистить таблицу лидеров?')) {
|
||
leaderboard = [];
|
||
localStorage.removeItem('pokerLeaderboard');
|
||
renderLeaderboard();
|
||
showNotification('Таблица лидеров очищена', 'success');
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// УТИЛИТЫ
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Выйти из игры
|
||
*/
|
||
function leaveGame() {
|
||
if (confirm('Вы уверены, что хотите выйти?')) {
|
||
if (isMultiplayer) {
|
||
leaveRoom();
|
||
}
|
||
game = null;
|
||
showScreen('main-menu');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Показать уведомление
|
||
*/
|
||
function showNotification(message, type = 'info') {
|
||
const container = document.getElementById('notifications');
|
||
|
||
const notification = document.createElement('div');
|
||
notification.className = `notification ${type}`;
|
||
notification.textContent = message;
|
||
|
||
container.appendChild(notification);
|
||
|
||
setTimeout(() => {
|
||
notification.remove();
|
||
}, 3000);
|
||
}
|