1085 lines
42 KiB
HTML
1085 lines
42 KiB
HTML
{% 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>
|
|
|
|
<!-- Индикатор готовности -->
|
|
{% 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>
|
|
// Глобальные переменные
|
|
// Не объявляем socket заново, он уже объявлен в base.html
|
|
// let socket = io(); // УБРАТЬ эту строку!
|
|
let roomCode = '{{ room.code }}';
|
|
let currentPlayerId = '{{ current_player.id }}';
|
|
let isAdmin = {{ 'true' if current_player.is_admin else 'false' }};
|
|
|
|
// Инициализация при загрузке
|
|
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);
|
|
showNotification(`🎮 ${data.username} присоединился к комнате`);
|
|
setTimeout(() => {
|
|
location.reload();
|
|
}, 1000);
|
|
});
|
|
|
|
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 => {
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
showNotification('✅ Игра начинается...', 'success');
|
|
} 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') {
|
|
const notification = document.createElement('div');
|
|
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;
|
|
`;
|
|
|
|
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);
|
|
|
|
setTimeout(() => {
|
|
notification.style.animation = 'slideOutRight 0.3s ease';
|
|
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();
|
|
});
|
|
}
|
|
</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;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %} |