Files
cm/templates/lobby.html
2026-02-03 18:42:12 +03:00

1366 lines
54 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Лобби: {{ room.name }} - Капитал & Рынок{% endblock %}
{% block content %}
<div class="screen active">
<!-- Шапка -->
<div class="header">
<a href="{{ url_for('rooms') }}" class="back-button"></a>
<div class="logo-container">
<span class="logo-text" style="color: white; font-weight: bold; font-size: 1.2rem;">
💰 Лобби
</span>
</div>
<div style="margin-left: auto;">
<span style="font-size: 0.9rem;">
{% if current_user.is_authenticated %}
{{ current_user.username }}
{% endif %}
</span>
</div>
</div>
<!-- Основной контент -->
<div class="container">
<!-- Информация о комнате -->
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
<h3 style="margin-bottom: 5px;">{{ room.name }}</h3>
<div style="color: var(--light-text); font-size: 0.9rem;">
Код комнаты: <strong>{{ room.code }}</strong>
<button onclick="copyRoomCode()"
style="background: none; border: none; color: var(--primary-color); cursor: pointer; margin-left: 5px;"
title="Скопировать код">
📋
</button>
</div>
</div>
<div class="room-status {{ room.status }}" style="margin-left: 10px;">
{% if room.status == 'waiting' %}
Ожидание
{% elif room.status == 'playing' %}
Игра идет
{% elif room.status == 'finished' %}
Завершена
{% else %}
{{ room.status }}
{% endif %}
</div>
</div>
<div style="margin: 15px 0; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
<div>
<div style="font-size: 0.9rem; color: var(--light-text);">Игроков:</div>
<div style="font-size: 1.2rem; font-weight: bold;">
{{ players|length }}/{{ config.MAX_PLAYERS_PER_ROOM }}
</div>
</div>
<div>
<div style="font-size: 0.9rem; color: var(--light-text);">Длительность:</div>
<div style="font-size: 1.2rem; font-weight: bold;">
{{ room.total_months }} месяцев
</div>
</div>
<div>
<div style="font-size: 0.9rem; color: var(--light-text);">Стартовый капитал:</div>
<div style="font-size: 1.2rem; font-weight: bold;">
{{ room.start_capital|format_currency }}
</div>
</div>
<div>
<div style="font-size: 0.9rem; color: var(--light-text);">Создатель:</div>
<div style="font-size: 1.1rem;">
{{ room.creator.username if room.creator else 'Неизвестно' }}
</div>
</div>
</div>
</div>
<!-- Настройки комнаты -->
{% if room.settings %}
{% set settings_dict = room.settings|from_json %}
<div style="margin-top: 15px;">
<h4>Настройки игры:</h4>
<div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px;">
{% if settings_dict.allow_loans %}
<span style="background-color: #e3f2fd; color: #1976d2; padding: 6px 12px; border-radius: 15px; font-size: 0.9rem;">
✅ Кредиты разрешены
</span>
{% else %}
<span style="background-color: #ffebee; color: #d32f2f; padding: 6px 12px; border-radius: 15px; font-size: 0.9rem;">
❌ Кредиты запрещены
</span>
{% endif %}
{% if settings_dict.allow_black_market %}
<span style="background-color: #f3e5f5; color: #7b1fa2; padding: 6px 12px; border-radius: 15px; font-size: 0.9rem;">
⚫ Чёрный рынок
</span>
{% else %}
<span style="background-color: #f5f5f5; color: #616161; padding: 6px 12px; border-radius: 15px; font-size: 0.9rem;">
⚪ Без чёрного рынка
</span>
{% endif %}
{% if settings_dict.private_room %}
<span style="background-color: #fff3e0; color: #ef6c00; padding: 6px 12px; border-radius: 15px; font-size: 0.9rem;">
🔒 Приватная
</span>
{% else %}
<span style="background-color: #e8f5e9; color: #388e3c; padding: 6px 12px; border-radius: 15px; font-size: 0.9rem;">
🔓 Публичная
</span>
{% endif %}
</div>
</div>
{% endif %}
<!-- Кнопки управления -->
<div style="margin-top: 20px; display: flex; gap: 10px; flex-wrap: wrap;">
{% if current_player.is_admin and room.status == 'waiting' %}
<button id="start-game-btn" onclick="startGame()" class="button success">
▶️ Начать игру
</button>
{% elif room.status == 'playing' %}
<button onclick="joinGame()" class="button">
🎮 Войти в игру
</button>
{% endif %}
{% if room.status == 'waiting' %}
<button id="ready-btn" onclick="toggleReady()"
class="button {% if current_player.is_ready %}success{% else %}secondary{% endif %}">
{% if current_player.is_ready %}
✅ Готов
{% else %}
Не готов
{% endif %}
</button>
{% endif %}
<button onclick="showSettings()" class="button secondary">
⚙️ Настройки
</button>
<button onclick="copyInviteLink()" class="button secondary">
📨 Пригласить
</button>
{% if current_player.is_admin %}
<button onclick="showAdminPanel()" class="button warning">
👑 Админ
</button>
{% endif %}
</div>
<div style="margin-top: 10px;">
<button onclick="exitLobby()" class="button danger" style="width: 100%;">
🚪 Выйти из лобби
</button>
</div>
<!-- Индикатор готовности -->
{% if room.status == 'waiting' %}
<div style="margin-top: 15px; padding: 10px; background-color: #f5f5f5; border-radius: var(--border-radius);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>Готовы к старту:</span>
<span id="ready-count">{{ players|selectattr('is_ready')|list|length }}/{{ players|length }}</span>
</div>
<div class="progress-bar" style="margin-top: 5px;">
<div class="progress-fill" id="ready-progress"
style="width: {{ (players|selectattr('is_ready')|list|length / players|length * 100)|round(1) if players|length > 0 else 0 }}%">
</div>
</div>
{% if players|length < 2 %}
<div style="margin-top: 10px; color: var(--warning-color); font-size: 0.9rem;">
⚠️ Нужно минимум 2 игрока для начала игры
</div>
{% endif %}
</div>
{% endif %}
</div>
<!-- Список игроков -->
<div class="card">
<h3>👥 Участники ({{ players|length }})</h3>
{% if players %}
<ul class="player-list" id="player-list">
{% for player in players %}
<li class="player-item">
<div class="player-info">
<div class="player-avatar"
style="background-color: {% if player.is_admin %}#ff9800{% else %}var(--primary-color){% endif %};">
{{ player.user.username[0]|upper if player.user and player.user.username else '?' }}
</div>
<div>
<div style="display: flex; align-items: center; gap: 5px;">
<strong>{{ player.user.username if player.user else 'Игрок' }}</strong>
{% if player.is_admin %}
<span style="background-color: #fff3e0; color: #ef6c00; padding: 2px 6px; border-radius: 10px; font-size: 0.75rem;">Админ</span>
{% endif %}
{% if player.user_id == current_user.id %}
<span style="background-color: #e3f2fd; color: #1976d2; padding: 2px 6px; border-radius: 10px; font-size: 0.75rem;">Вы</span>
{% endif %}
</div>
<div class="player-capital">
{{ player.capital|format_currency }}
</div>
</div>
</div>
<div>
{% if room.status == 'waiting' %}
<div class="player-ready {% if player.is_ready %}ready{% else %}not-ready{% endif %}">
{% if player.is_ready %}
✅ Готов
{% else %}
⏳ Ожидание
{% endif %}
</div>
{% endif %}
<div class="player-ability" title="{{ get_ability_description(player.ability) }}">
{{ get_ability_name(player.ability) }}
</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<div style="text-align: center; padding: 20px; color: var(--light-text);">
<div style="font-size: 3rem; margin-bottom: 10px;">👤</div>
<p>В комнате пока нет игроков</p>
<p style="font-size: 0.9rem;">Пригласите друзей!</p>
</div>
{% endif %}
</div>
<!-- Чат комнаты -->
<div class="card">
<h3>💬 Чат комнаты</h3>
<div id="chat-messages"
style="height: 200px; overflow-y: auto; margin-bottom: 10px; padding: 10px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="text-align: center; color: var(--light-text); padding: 20px;">
Чат загружается...
</div>
</div>
<div style="display: flex; gap: 10px;">
<input type="text" id="chat-input" placeholder="Введите сообщение..."
style="flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: var(--border-radius);"
onkeypress="if(event.key === 'Enter') sendMessage()">
<button onclick="sendMessage()" class="button" style="padding: 10px 20px;">
📤
</button>
</div>
</div>
<!-- Статистика комнаты -->
<div class="card">
<h3>📊 Статистика комнаты</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-top: 15px;">
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">{{ players|length }}
</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Игроков</div>
</div>
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">{{ room.current_month
}}
</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Текущий месяц</div>
</div>
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">
{{ (players|map(attribute='capital')|sum)|format_currency }}
</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Общий капитал</div>
</div>
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">
{% if players|length > 0 %}
{{ ((players|selectattr('is_ready')|list|length / players|length * 100)|round(1)) }}%
{% else %}
0%
{% endif %}
</div>
<div style="font-size: 0.9rem; color: var(--light-text);">Готовы</div>
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно настроек -->
<div id="settings-modal" class="modal-backdrop">
<div class="modal">
<h3>⚙️ Настройки комнаты</h3>
{% if current_player.is_admin and room.status == 'waiting' %}
<form id="room-settings-form" onsubmit="return updateRoomSettings(event)">
<div class="input-group">
<label for="edit-room-name">Название комнаты</label>
<input type="text" id="edit-room-name" name="name" value="{{ room.name }}" required>
</div>
<div class="input-group">
<label for="edit-total-months">Длительность игры</label>
<select id="edit-total-months" name="total_months">
<option value="6" {% if room.total_months== 6 %}selected{% endif %}>6 месяцев</option>
<option value="12" {% if room.total_months== 12 %}selected{% endif %}>12 месяцев</option>
<option value="18" {% if room.total_months== 18 %}selected{% endif %}>18 месяцев</option>
<option value="24" {% if room.total_months== 24 %}selected{% endif %}>24 месяца</option>
</select>
</div>
<div class="input-group">
<label for="edit-start-capital">Стартовый капитал</label>
<input type="number" id="edit-start-capital" name="start_capital"
value="{{ room.start_capital }}" min="50000" max="1000000" step="10000">
</div>
<div class="input-group">
<label style="display: flex; align-items: center; gap: 10px;">
{% if room.settings %}
{% set settings_dict = room.settings|from_json %}
<input type="checkbox" id="edit-allow-loans" name="allow_loans"
{% if settings_dict.allow_loans %}checked{% endif %}>
{% else %}
<input type="checkbox" id="edit-allow-loans" name="allow_loans">
{% endif %}
<span>Разрешить кредиты</span>
</label>
</div>
<div class="input-group">
<label style="display: flex; align-items: center; gap: 10px;">
{% if room.settings %}
{% set settings_dict = room.settings|from_json %}
<input type="checkbox" id="edit-allow-black-market" name="allow_black_market"
{% if settings_dict.allow_black_market %}checked{% endif %}>
{% else %}
<input type="checkbox" id="edit-allow-black-market" name="allow_black_market">
{% endif %}
<span>Разрешить чёрный рынок</span>
</label>
</div>
<div class="flex gap-2 mt-4">
<button type="submit" class="button success">Сохранить</button>
<button type="button" onclick="hideSettings()" class="button secondary">Отмена</button>
</div>
</form>
{% else %}
<div style="padding: 20px; text-align: center;">
<p>Только администратор может изменять настройки комнаты.</p>
<button onclick="hideSettings()" class="button mt-3">Закрыть</button>
</div>
{% endif %}
</div>
</div>
<!-- Модальное окно приглашения -->
<div id="invite-modal" class="modal-backdrop">
<div class="modal">
<h3>📨 Пригласить в комнату</h3>
<div style="margin: 15px 0;">
<p>Отправьте эту ссылку друзьям:</p>
<div style="display: flex; gap: 10px; margin: 10px 0;">
<input type="text" id="invite-link" readonly
value="{{ request.url_root }}room/{{ room.code }}"
style="flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: var(--border-radius);">
<button onclick="copyInviteLinkText()" class="button">Копировать</button>
</div>
<p style="margin-top: 20px;">Или QR-код:</p>
<div id="qrcode" style="text-align: center; margin: 15px 0;">
<!-- QR-код будет сгенерирован JavaScript -->
<div style="background-color: white; padding: 10px; display: inline-block; border-radius: var(--border-radius);">
<div style="font-size: 0.8rem; color: var(--light-text); margin-bottom: 5px;">Отсканируйте QR-код
</div>
<div style="width: 150px; height: 150px; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; border-radius: 5px;">
<div style="text-align: center;">
<div style="font-size: 3rem;">📱</div>
<div style="font-size: 0.7rem; color: var(--light-text); margin-top: 5px;">{{ room.code }}
</div>
</div>
</div>
</div>
</div>
</div>
<button onclick="hideInviteModal()" class="button">Закрыть</button>
</div>
</div>
<!-- Модальное окно администратора -->
<div id="admin-modal" class="modal-backdrop">
<div class="modal">
<h3>👑 Панель администратора</h3>
<div style="margin: 15px 0;">
<h4>Управление игроками:</h4>
<div id="admin-player-list"
style="max-height: 200px; overflow-y: auto; margin: 10px 0; padding: 10px; background-color: #f5f5f5; border-radius: var(--border-radius);">
{% for player in players %}
{% if not player.is_admin %}
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #ddd;">
<span>{{ player.user.username if player.user else 'Игрок' }}</span>
<button onclick="kickPlayer({{ player.user_id }})" class="button danger"
style="padding: 5px 10px; font-size: 0.8rem;">
Выгнать
</button>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<div style="margin: 15px 0;">
<h4>Опасные действия:</h4>
<div style="display: flex; flex-direction: column; gap: 10px;">
<button onclick="kickAllPlayers()" class="button danger">
🚫 Выгнать всех игроков
</button>
<button onclick="resetRoom()" class="button danger">
🔄 Сбросить комнату
</button>
<button onclick="deleteRoom()" class="button danger">
🗑️ Удалить комнату
</button>
</div>
</div>
<button onclick="hideAdminModal()" class="button">Закрыть</button>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Глобальные переменные
let roomCode = '{{ room.code }}';
let currentPlayerId = '{{ current_player.id }}';
let isAdmin = {{ 'true' if current_player.is_admin else 'false' }};
let lastNotificationTime = 0; // Для ограничения частоты уведомлений
window.roomStatus = '{{ room.status }}'; // Добавьте эту строку
window.userExitedLobby = false;
// Функция форматирования валюты
function formatCurrency(amount) {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0
}).format(amount);
}
// Инициализация при загрузке
document.addEventListener('DOMContentLoaded', function() {
console.log('Lobby page loaded for room:', roomCode);
// Проверяем, что socket существует
if (typeof socket !== 'undefined' && roomCode) {
socket.emit('join_room', { room: roomCode });
console.log('Joined room via WebSocket:', roomCode);
} else {
console.warn('WebSocket не инициализирован');
}
// Загружаем историю чата
loadChatHistory();
// Периодическое обновление статуса
setInterval(updateRoomStatus, 5000);
});
// Функции управления модальными окнами
function showSettings() {
console.log('Opening settings modal');
document.getElementById('settings-modal').classList.add('active');
}
function hideSettings() {
console.log('Closing settings modal');
document.getElementById('settings-modal').classList.remove('active');
}
function copyInviteLink() {
console.log('Opening invite modal');
document.getElementById('invite-modal').classList.add('active');
}
function hideInviteModal() {
console.log('Closing invite modal');
document.getElementById('invite-modal').classList.remove('active');
}
function showAdminPanel() {
if (!isAdmin) {
showNotification('❌ Только для администратора', 'error');
return;
}
console.log('Opening admin panel');
document.getElementById('admin-modal').classList.add('active');
}
function hideAdminModal() {
console.log('Closing admin panel');
document.getElementById('admin-modal').classList.remove('active');
}
// WebSocket обработчики - только если socket существует
if (typeof socket !== 'undefined') {
socket.on('connect', function() {
console.log('Connected to WebSocket server');
if (roomCode) {
socket.emit('join_room', { room: roomCode });
}
});
socket.on('player_joined', function(data) {
console.log('Player joined:', data.username, data);
// Проверяем, не текущий ли это пользователь
if (data.user_id && data.user_id == currentPlayerId) {
console.log('Это текущий пользователь, игнорируем');
return;
}
// Проверяем, не было ли уже уведомления
if (window.lastJoinNotification === data.user_id + '_' + new Date().toISOString().slice(0, 10)) {
console.log('Уведомление уже показывалось сегодня');
return;
}
// Показываем уведомление
showNotification(`🎮 ${data.username} присоединился к комнате`);
// Обновляем список игроков без перезагрузки
setTimeout(() => {
updatePlayerList();
}, 500);
// Запоминаем, что показали уведомление
window.lastJoinNotification = data.user_id + '_' + new Date().toISOString().slice(0, 10);
});
socket.on('player_reconnected', function(data) {
console.log('Player reconnected:', data.username);
// Не показываем уведомление для реконнекта
// Просто обновляем список
setTimeout(() => {
updatePlayerList();
}, 500);
});
socket.on('player_left', function(data) {
console.log('Player left:', data.username);
showNotification(`🚪 ${data.username} покинул комнату`);
setTimeout(() => {
location.reload();
}, 1000);
});
socket.on('player_ready_changed', function(data) {
console.log('Player ready changed:', data.username, data.is_ready);
updateReadyStatus();
setTimeout(() => {
location.reload();
}, 500);
});
socket.on('chat_message', function(data) {
console.log('Chat message:', data);
addChatMessage(data);
});
socket.on('game_started', function(data) {
console.log('Game started:', data);
showNotification('🎉 Игра началась! Перенаправление...');
setTimeout(() => {
window.location.href = `/game/{{ room.code }}`;
}, 2000);
});
socket.on('room_updated', function(data) {
console.log('Room updated:', data);
showNotification('⚙️ Настройки комнаты обновлены');
setTimeout(() => {
location.reload();
}, 1000);
});
} else {
console.warn('WebSocket не доступен, некоторые функции могут не работать');
}
// Функции управления комнатой
function copyRoomCode() {
navigator.clipboard.writeText('{{ room.code }}').then(() => {
showNotification('✅ Код комнаты скопирован');
}).catch(err => {
console.error('Failed to copy: ', err);
showNotification('❌ Ошибка при копировании', 'error');
});
}
function startGame() {
if (!isAdmin) {
showNotification('❌ Только администратор может начать игру', 'error');
return;
}
const readyPlayers = {{ players|selectattr('is_ready')|list|length }};
const totalPlayers = {{ players|length }};
if (totalPlayers < 2) {
showNotification('❌ Нужно минимум 2 игрока для начала игры', 'error');
return;
}
if (readyPlayers < totalPlayers) {
if (!confirm(`Не все игроки готовы (${readyPlayers}/${totalPlayers}). Начать игру?`)) {
return;
}
}
fetch('/room/{{ room.code }}/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('✅ Игра начинается!', 'success');
// Ждем 2 секунды и перенаправляем в игру
setTimeout(() => {
window.location.href = '/game/{{ room.code }}';
}, 2000);
} else {
showNotification('❌ Ошибка: ' + (data.error || 'Неизвестная ошибка'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Ошибка при запуске игры', 'error');
});
}
function joinGame() {
window.location.href = `/game/{{ room.code }}`;
}
function toggleReady() {
const isReady = document.getElementById('ready-btn').classList.contains('success');
const newReadyState = !isReady;
fetch('/room/{{ room.code }}/ready', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
},
body: JSON.stringify({ ready: newReadyState })
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success) {
const btn = document.getElementById('ready-btn');
if (data.is_ready) {
btn.classList.remove('secondary');
btn.classList.add('success');
btn.innerHTML = '✅ Готов';
} else {
btn.classList.remove('success');
btn.classList.add('secondary');
btn.innerHTML = '⏳ Не готов';
}
// Отправляем через WebSocket если доступен
if (typeof socket !== 'undefined') {
socket.emit('player_ready', {
room: roomCode,
ready: data.is_ready
});
}
showNotification(data.is_ready ? '✅ Вы готовы к игре' : '⏳ Вы не готовы', 'success');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Ошибка при изменении статуса', 'error');
});
}
function updateRoomSettings(event) {
event.preventDefault();
const form = document.getElementById('room-settings-form');
const formData = new FormData(form);
fetch('/room/{{ room.code }}/update', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success) {
showNotification('✅ Настройки сохранены', 'success');
hideSettings();
if (typeof socket !== 'undefined') {
socket.emit('room_updated', { room: roomCode });
}
} else {
showNotification('❌ Ошибка: ' + (data.error || 'Неизвестная ошибка'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Ошибка при сохранении настроек', 'error');
});
}
function copyInviteLinkText() {
const linkInput = document.getElementById('invite-link');
linkInput.select();
linkInput.setSelectionRange(0, 99999);
navigator.clipboard.writeText(linkInput.value).then(() => {
showNotification('✅ Ссылка скопирована', 'success');
}).catch(err => {
document.execCommand('copy');
showNotification('✅ Ссылка скопирована', 'success');
});
}
// Функции администрирования
function kickPlayer(userId) {
if (!confirm('Вы уверены, что хотите выгнать этого игрока?')) {
return;
}
fetch(`/room/{{ room.code }}/kick/${userId}`, {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('✅ Игрок выгнан', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showNotification('❌ Ошибка: ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Ошибка при выгонении игрока', 'error');
});
}
function kickAllPlayers() {
if (!confirm('ВЫ УВЕРЕНЫ? Это выгонит всех игроков из комнаты!')) {
return;
}
fetch('/room/{{ room.code }}/kick_all', {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('✅ Все игроки выгнаны', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showNotification('❌ Ошибка: ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Ошибка при выгонении игроков', 'error');
});
}
function resetRoom() {
if (!confirm('ВЫ УВЕРЕНЫ? Это сбросит комнату к начальному состоянию!')) {
return;
}
fetch('/room/{{ room.code }}/reset', {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('✅ Комната сброшена', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showNotification('❌ Ошибка: ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Ошибка при сбросе комнаты', 'error');
});
}
function deleteRoom() {
if (!confirm('ВЫ УВЕРЕНЫ? Это УДАЛИТ комнату навсегда!')) {
return;
}
fetch('/room/{{ room.code }}/delete', {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('✅ Комната удалена', 'success');
setTimeout(() => {
window.location.href = '/rooms';
}, 1000);
} else {
showNotification('❌ Ошибка: ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Ошибка при удалении комнаты', 'error');
});
}
// Функции чата
function loadChatHistory() {
const chatContainer = document.getElementById('chat-messages');
chatContainer.innerHTML = '<div style="text-align: center; color: var(--light-text); padding: 20px;">Нет сообщений</div>';
}
function sendMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) return;
// Отправляем через WebSocket если доступен
if (typeof socket !== 'undefined') {
socket.emit('chat_message', {
room: roomCode,
message: message,
username: '{{ current_user.username }}',
user_id: currentPlayerId,
timestamp: new Date().toISOString()
});
}
addChatMessage({
username: '{{ current_user.username }} (Вы)',
user_id: currentPlayerId,
message: message,
timestamp: new Date().toISOString()
});
input.value = '';
input.focus();
}
function addChatMessage(data) {
const chatContainer = document.getElementById('chat-messages');
if (chatContainer.innerHTML.includes('Нет сообщений') || chatContainer.innerHTML.includes('Чат загружается')) {
chatContainer.innerHTML = '';
}
const messageElement = document.createElement('div');
messageElement.style.marginBottom = '10px';
messageElement.style.padding = '8px 12px';
messageElement.style.backgroundColor = data.user_id == currentPlayerId ? '#e3f2fd' : 'white';
messageElement.style.borderRadius = '10px';
messageElement.style.border = '1px solid #eee';
messageElement.style.wordBreak = 'break-word';
const time = new Date(data.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
messageElement.innerHTML = `
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
<strong>${data.username}</strong>
<span style="color: var(--light-text); font-size: 0.8rem;">${time}</span>
</div>
<div>${data.message}</div>
`;
chatContainer.appendChild(messageElement);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// Вспомогательные функции
function updateReadyStatus() {
const readyPlayers = document.querySelectorAll('.player-ready.ready').length;
const totalPlayers = document.querySelectorAll('.player-item').length;
const readyCount = document.getElementById('ready-count');
const readyProgress = document.getElementById('ready-progress');
if (readyCount) readyCount.textContent = `${readyPlayers}/${totalPlayers}`;
if (readyProgress) {
const progress = totalPlayers > 0 ? (readyPlayers / totalPlayers * 100) : 0;
readyProgress.style.width = `${progress}%`;
}
}
function updateRoomStatus() {
fetch(`/api/room/{{ room.code }}/status`)
.then(response => {
if (!response.ok) return;
return response.json();
})
.then(data => {
if (data && data.status !== '{{ room.status }}') {
location.reload();
}
})
.catch(error => console.error('Error updating room status:', error));
}
function showNotification(message, type = 'info') {
// Ограничиваем частоту уведомлений (не чаще 1 раза в 3 секунды)
const now = Date.now();
if (now - lastNotificationTime < 3000) {
console.log('Notification throttled');
return;
}
lastNotificationTime = now;
// Скрываем предыдущие уведомления
const oldNotifications = document.querySelectorAll('.custom-notification');
oldNotifications.forEach(n => n.remove());
const notification = document.createElement('div');
notification.className = 'custom-notification';
notification.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
background: white;
padding: 15px 20px;
border-radius: var(--border-radius);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
border-left: 4px solid var(--primary-color);
z-index: 10000;
animation: slideInRight 0.3s ease;
max-width: 300px;
word-break: break-word;
transition: opacity 0.3s;
`;
if (type === 'success') notification.style.borderLeftColor = 'var(--success-color)';
if (type === 'error') notification.style.borderLeftColor = 'var(--danger-color)';
if (type === 'warning') notification.style.borderLeftColor = 'var(--warning-color)';
notification.textContent = message;
document.body.appendChild(notification);
// Автоматическое скрытие через 3 секунды
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Обработка клавиш
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
hideSettings();
hideInviteModal();
hideAdminModal();
}
if (event.key === '/' && !event.target.matches('input, textarea')) {
event.preventDefault();
const chatInput = document.getElementById('chat-input');
if (chatInput) chatInput.focus();
}
});
// Закрытие модальных окон при клике на фон
document.addEventListener('click', function(event) {
if (event.target.classList.contains('modal-backdrop')) {
hideSettings();
hideInviteModal();
hideAdminModal();
}
});
// Фокус на поле ввода чата
const chatMessages = document.getElementById('chat-messages');
if (chatMessages) {
chatMessages.addEventListener('click', function() {
const chatInput = document.getElementById('chat-input');
if (chatInput) chatInput.focus();
});
}
window.addEventListener('beforeunload', function(e) {
// Только если WebSocket подключен и мы в комнате
if (typeof socket !== 'undefined' && socket.connected && roomCode) {
// Не показываем подтверждение, если пользователь сам нажал выход
if (!window.userExitedLobby) {
// Отправляем событие выхода
socket.emit('leave_room', { room: roomCode });
}
}
});
// Обработка видимости страницы (если переключились на другую вкладку)
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
console.log('Page is now hidden');
} else {
console.log('Page is now visible');
// При возвращении на страницу проверяем статус
updateRoomStatus();
}
});
// Функция обновления списка игроков без перезагрузки страницы
function updatePlayerList() {
console.log('Updating player list...');
fetch(`/api/room/${roomCode}/players`)
.then(response => response.json())
.then(data => {
if (data.success && data.players) {
updatePlayerListUI(data.players);
updateReadyStatus();
}
})
.catch(error => {
console.error('Error updating player list:', error);
// В случае ошибки делаем мягкую перезагрузку через 2 секунды
setTimeout(() => {
location.reload();
}, 2000);
});
}
// Функция обновления UI списка игроков
function updatePlayerListUI(players) {
const playerList = document.getElementById('player-list');
if (!playerList) return;
playerList.innerHTML = '';
players.forEach(player => {
const li = document.createElement('li');
li.className = 'player-item';
li.innerHTML = `
<div class="player-info">
<div class="player-avatar" style="background-color: ${player.is_admin ? '#ff9800' : 'var(--primary-color)'};">
${player.username ? player.username[0].toUpperCase() : '?'}
</div>
<div>
<div style="display: flex; align-items: center; gap: 5px;">
<strong>${player.username || 'Игрок'}</strong>
${player.is_admin ? '<span style="background-color: #fff3e0; color: #ef6c00; padding: 2px 6px; border-radius: 10px; font-size: 0.75rem;">Админ</span>' : ''}
${player.user_id == currentPlayerId ? '<span style="background-color: #e3f2fd; color: #1976d2; padding: 2px 6px; border-radius: 10px; font-size: 0.75rem;">Вы</span>' : ''}
</div>
<div class="player-capital">
${formatCurrency(player.capital)}
</div>
</div>
</div>
<div>
${window.roomStatus === 'waiting' ? `
<div class="player-ready ${player.is_ready ? 'ready' : 'not-ready'}">
${player.is_ready ? '✅ Готов' : '⏳ Ожидание'}
</div>
` : ''}
<div class="player-ability" title="${player.ability_description}">
${player.ability_name}
</div>
</div>
`;
playerList.appendChild(li);
});
}
// Обработка дисконнекта WebSocket
socket.on('disconnect', function(reason) {
console.log('WebSocket disconnected:', reason);
if (reason === 'io server disconnect') {
// Сервер намеренно отключил
showNotification('🔌 Соединение с сервером потеряно. Переподключаемся...', 'warning');
}
});
// Периодическая проверка подключения
setInterval(() => {
if (socket && !socket.connected) {
console.log('Socket not connected, attempting to reconnect...');
socket.connect();
}
}, 5000);
function exitLobby() {
if (!confirm('Вы уверены, что хотите выйти из лобби?')) {
return;
}
window.userExitedLobby = true;
fetch('/room/{{ room.code }}/exit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success) {
showNotification('✅ Вы вышли из лобби', 'success');
// Отправляем WebSocket событие если подключены
if (typeof socket !== 'undefined' && socket.connected) {
socket.emit('leave_room', { room: roomCode });
}
// Редирект через 1 секунду
setTimeout(() => {
window.location.href = '{{ url_for("rooms") }}';
}, 1000);
} else {
showNotification('❌ Ошибка: ' + (data.error || 'Неизвестная ошибка'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Ошибка при выходе из лобби', 'error');
});
}
</script>
<style>
.player-ready {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
text-align: center;
margin-bottom: 5px;
}
.player-ready.ready {
background-color: #e8f5e9;
color: #388e3c;
}
.player-ready.not-ready {
background-color: #ffebee;
color: #d32f2f;
}
.player-ability {
background-color: var(--secondary-color);
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8rem;
color: var(--light-text);
text-align: center;
cursor: help;
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.button.danger {
background-color: var(--danger-color);
}
.button.danger:hover {
background-color: #c62828;
}
.button.warning {
background-color: var(--warning-color);
}
.button.warning:hover {
background-color: #f57c00;
}
.modal-backdrop {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-backdrop.active {
display: flex;
}
.modal {
background-color: white;
border-radius: var(--border-radius);
padding: 20px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideOutRight {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(100%);
}
}
/* Адаптивные стили */
@media (max-width: 480px) {
.player-item {
flex-direction: column;
align-items: flex-start;
}
.player-ready, .player-ability {
align-self: flex-end;
margin-top: 5px;
}
#chat-messages {
height: 150px;
}
.modal {
width: 95%;
padding: 15px;
}
}
.button.danger {
background-color: var(--danger-color);
color: white;
border: none;
}
.button.danger:hover {
background-color: #d32f2f;
transform: translateY(-2px);
}
.button.danger:active {
transform: translateY(0);
}
.exit-confirmation {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
display: none;
}
.exit-confirmation.active {
display: flex;
}
.exit-modal {
background-color: white;
padding: 25px;
border-radius: var(--border-radius);
max-width: 400px;
width: 90%;
text-align: center;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.3);
}
.exit-modal h3 {
color: var(--danger-color);
margin-bottom: 15px;
}
.exit-modal p {
margin-bottom: 20px;
color: var(--text-color);
line-height: 1.5;
}
.exit-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.exit-buttons .button {
flex: 1;
}
</style>
{% endblock %}