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

1275 lines
46 KiB
HTML
Raw Permalink 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="#" class="back-button" onclick="showExitModal(event)">
</a>
<div class="logo-container">
<span class="logo-text" style="color: white; font-weight: bold; font-size: 1.1rem;">
🎮 {{ room.name }}
</span>
</div>
<div style="display: flex; align-items: center; gap: 15px; margin-left: auto;">
<div class="timer" id="phase-timer">
{{ game_state.phase|capitalize }}: 2:00
</div>
<div style="color: white; font-weight: 600; font-size: 1rem;">
Месяц {{ room.current_month }}/{{ room.total_months }}
</div>
</div>
</div>
<!-- Основной контент игры -->
<div class="container">
<!-- Верхняя панель с информацией игрока -->
<div class="card" style="margin-bottom: 10px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 15px;">
<div class="player-avatar" style="width: 50px; height: 50px; font-size: 1.3rem;">
{{ current_user.username[0]|upper }}
</div>
<div>
<h3 style="margin: 0;">{{ current_user.username }}</h3>
<div style="color: var(--light-text); font-size: 0.9rem;">
{{ get_ability_name(player.ability) }}
</div>
</div>
</div>
<div style="text-align: right;">
<div style="font-size: 1.8rem; font-weight: bold; color: var(--success-color);">
{{ player.capital|format_currency }}
</div>
<div style="font-size: 0.9rem; color: var(--light-text);">
Капитал
</div>
</div>
</div>
<!-- Прогресс капитала -->
<div style="margin-top: 15px;">
<div class="progress-label">
<span>Прогресс капитала</span>
<span id="capital-progress-text">0%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="capital-progress"></div>
</div>
<div style="font-size: 0.8rem; color: var(--light-text); text-align: center; margin-top: 5px;">
Цель: 500,000 ₽
</div>
</div>
</div>
<!-- Табы для разных фаз игры -->
<div class="tab-container">
<div class="tab active" data-tab="action" id="tab-action">
🎯 Действия
</div>
<div class="tab" data-tab="market" id="tab-market">
📊 Рынок
</div>
<div class="tab" data-tab="events" id="tab-events">
📰 События
</div>
<div class="tab" data-tab="results" id="tab-results">
🏆 Итоги
</div>
</div>
<!-- Контент табов -->
<div id="action-tab" class="tab-content active">
<!-- Фаза действий -->
<div class="card">
<h3>💼 Инвестиционные возможности</h3>
<p style="margin-bottom: 15px; color: var(--light-text);">
У вас есть {{ player.capital|format_currency }} для инвестиций.
Выберите активы для покупки или продажи.
</p>
<!-- Активы для покупки -->
<div class="asset-list" id="assets-list">
<!-- Активы будут загружены через JavaScript -->
<div style="text-align: center; padding: 20px; color: var(--light-text);">
Загрузка активов...
</div>
</div>
<!-- Кнопка завершения хода -->
<button id="end-turn-btn" onclick="endActionPhase()" class="button success" style="margin-top: 20px;">
✅ Завершить ход
</button>
<!-- Кнопка пропуска хода -->
<button onclick="skipTurn()" class="button secondary" style="margin-top: 10px;">
⏭ Пропустить ход
</button>
</div>
<!-- Способности игрока -->
<div class="card">
<h3>✨ Ваша способность: {{ get_ability_name(player.ability) }}</h3>
<p style="margin-bottom: 15px; color: var(--light-text);">
{{ get_ability_description(player.ability) }}
</p>
<button id="use-ability-btn" onclick="useAbility()" class="button warning" style="width: 100%;">
🎯 Использовать способность
</button>
<div id="ability-cooldown" style="margin-top: 10px; font-size: 0.9rem; color: var(--light-text); text-align: center;">
<!-- Информация о перезарядке -->
</div>
</div>
<!-- Ваши активы -->
<div class="card">
<h3>📈 Ваши активы</h3>
<div id="player-assets" style="min-height: 100px;">
{% if player.assets and player.assets != '[]' %}
{% set assets = player.assets|from_json %}
{% for asset in assets %}
<div class="asset-item">
<div class="asset-info">
<div class="asset-name">
{{ asset_names.get(asset.id, asset.id)|capitalize }}
</div>
<div class="asset-meta">
{{ asset.quantity }} шт. •
Куплено за: {{ asset.purchase_price|format_currency }}
</div>
</div>
<div>
<button onclick="sellAsset('{{ asset.id }}', {{ asset.quantity }})"
class="button danger" style="padding: 8px 15px; font-size: 0.9rem;">
Продать
</button>
</div>
</div>
{% endfor %}
{% 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>
</div>
<!-- Таб рынка -->
<div id="market-tab" class="tab-content">
<div class="card">
<h3>📊 Текущий рынок</h3>
<p style="margin-bottom: 15px; color: var(--light-text);">
Цены активов после действий игроков
</p>
<div id="market-data">
<!-- Данные рынка будут загружены через JavaScript -->
<div style="text-align: center; padding: 20px; color: var(--light-text);">
Ожидание реакции рынка...
</div>
</div>
</div>
<div class="card">
<h3>📈 Графики изменений</h3>
<div style="height: 200px; background-color: #f8f9fa; border-radius: var(--border-radius);
display: flex; align-items: center; justify-content: center;">
<div style="text-align: center; color: var(--light-text);">
<div style="font-size: 3rem;">📊</div>
<p>Графики будут доступны после реакции рынка</p>
</div>
</div>
</div>
</div>
<!-- Таб событий -->
<div id="events-tab" class="tab-content">
<div class="card">
<h3>📰 Случайные события</h3>
<p style="margin-bottom: 15px; color: var(--light-text);">
События, которые повлияли на рынок в этом месяце
</p>
<div id="current-events">
<!-- События будут загружены через JavaScript -->
<div style="text-align: center; padding: 20px; color: var(--light-text);">
Ожидание случайных событий...
</div>
</div>
</div>
<div class="card">
<h3>🎲 Возможные события</h3>
<div class="news-item">
<div class="news-title">Экономические события:</div>
<ul style="padding-left: 20px; margin-top: 10px;">
<li>Бум нефти (+30% к нефти, -15% к рублю)</li>
<li>Кибератака (-50% к криптовалюте)</li>
<li>Инфляция (-10% ко всем активам)</li>
</ul>
</div>
<div class="news-item">
<div class="news-title">Политические события:</div>
<ul style="padding-left: 20px; margin-top: 10px;">
<li>Выборы президента (изменение налогов)</li>
<li>Международные санкции</li>
<li>Социальные протесты</li>
</ul>
</div>
</div>
</div>
<!-- Таб итогов -->
<div id="results-tab" class="tab-content">
<div class="card">
<h3>🏆 Итоги месяца {{ room.current_month }}</h3>
<div id="month-results">
<!-- Итоги будут загружены через JavaScript -->
<div style="text-align: center; padding: 20px; color: var(--light-text);">
Ожидание подведения итогов...
</div>
</div>
</div>
<div class="card">
<h3>📊 Лидерборд</h3>
<div id="leaderboard">
<!-- Таблица лидеров будет загружена через JavaScript -->
<div style="text-align: center; padding: 20px; color: var(--light-text);">
Загрузка лидерборда...
</div>
</div>
<button onclick="updateLeaderboard()" class="button secondary" style="margin-top: 15px;">
🔄 Обновить
</button>
</div>
<div class="card">
<h3>💰 Ваша прибыль/убыток</h3>
<div style="text-align: center; padding: 20px;">
<div id="profit-loss" style="font-size: 2rem; font-weight: bold; color: var(--success-color);">
+0 ₽
</div>
<div style="font-size: 0.9rem; color: var(--light-text); margin-top: 5px;">
Изменение капитала за месяц
</div>
</div>
</div>
</div>
<!-- Панель чата в игре -->
<div class="card" style="margin-top: 15px;">
<h3>💬 Игровой чат</h3>
<div id="game-chat-messages" style="height: 150px; 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="game-chat-input" placeholder="Сообщение для игроков..."
style="flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: var(--border-radius);"
onkeypress="if(event.key === 'Enter') sendGameMessage()">
<button onclick="sendGameMessage()" class="button" style="padding: 10px 20px;">
📤
</button>
</div>
</div>
</div>
</div>
<!-- Модальное окно подтверждения выхода -->
<div id="exit-game-modal" class="modal-backdrop">
<div class="modal" style="max-width: 400px; text-align: center;">
<div style="font-size: 3rem; margin-bottom: 15px;">⚠️</div>
<h3 style="color: var(--warning-color);">Выйти из игры?</h3>
<p style="margin: 15px 0; color: var(--light-text);">
Если вы выйдете сейчас, ваш прогресс будет сохранен.
</p>
<div style="margin: 20px 0; padding: 15px; background-color: #fff3e0; border-radius: var(--border-radius);">
<p style="margin: 0; font-size: 0.9rem; color: var(--warning-color);">
<strong>Внимание!</strong><br>
Игра продолжится без вас. Вы сможете вернуться в лобби.
</p>
</div>
<div class="flex gap-2">
<button onclick="confirmExitGameAction()" class="button danger" style="flex: 1;">
Выйти
</button>
<button onclick="cancelExitGame()" class="button secondary" style="flex: 1;">
Остаться
</button>
</div>
</div>
</div>
<!-- Модальное окно покупки актива -->
<div id="buy-asset-modal" class="modal-backdrop">
<div class="modal">
<h3 id="buy-asset-title">Покупка актива</h3>
<div style="margin: 20px 0;">
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>Цена за единицу:</span>
<strong id="asset-price">0 ₽</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>Ваш капитал:</span>
<strong id="player-capital-display">{{ player.capital|format_currency }}</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<span>Доступно:</span>
<strong id="max-quantity">0 шт.</strong>
</div>
<div class="input-group">
<label for="buy-quantity">Количество</label>
<input type="number" id="buy-quantity" min="1" value="1" style="width: 100%;">
</div>
<div style="margin-top: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="display: flex; justify-content: space-between;">
<span>Общая стоимость:</span>
<strong id="total-cost">0 ₽</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 5px;">
<span>Останется:</span>
<strong id="remaining-capital">0 ₽</strong>
</div>
</div>
</div>
<div class="flex gap-2">
<button onclick="confirmBuyAsset()" class="button success" style="flex: 1;">
Купить
</button>
<button onclick="cancelBuyAsset()" class="button secondary" style="flex: 1;">
Отмена
</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Глобальные переменные
const roomCode = '{{ room.code }}';
const playerId = {{ player.id }};
const playerCapital = {{ player.capital }};
let currentPhase = '{{ game_state.phase }}';
let phaseEndTime = '{{ game_state.phase_end }}';
let selectedAsset = null;
let gameTimer = null;
// Инициализация при загрузке
document.addEventListener('DOMContentLoaded', function() {
console.log('Game page loaded for room:', roomCode);
// Подключаемся к WebSocket комнате игры
if (typeof socket !== 'undefined' && roomCode) {
socket.emit('join_game_room', { room: roomCode });
console.log('Joined game room via WebSocket:', roomCode);
}
// Инициализация табов
initTabs();
// Загрузка данных игры
loadGameData();
// Обновление прогресс-бара капитала
updateCapitalProgress();
// Настройка таймера фазы
setupPhaseTimer();
// Загрузка активов для покупки
loadAssets();
// Загрузка лидерборда
updateLeaderboard();
// Загрузка истории чата
loadGameChat();
// Автоматическое обновление каждые 10 секунд
setInterval(updateGameState, 10000);
});
// WebSocket обработчики для игры
if (typeof socket !== 'undefined') {
socket.on('game_phase_changed', function(data) {
console.log('Game phase changed:', data);
currentPhase = data.phase;
phaseEndTime = data.phase_end;
updatePhaseDisplay();
setupPhaseTimer();
showNotification(`📢 Новая фаза: ${getPhaseName(data.phase)}`, 'info');
});
socket.on('market_updated', function(data) {
console.log('Market updated:', data);
updateMarketDisplay(data.assets);
});
socket.on('game_event', function(data) {
console.log('Game event:', data);
showGameEvent(data);
});
socket.on('player_action', function(data) {
console.log('Player action:', data);
showPlayerAction(data);
});
socket.on('game_chat_message', function(data) {
console.log('Game chat message:', data);
addGameChatMessage(data);
});
socket.on('game_ended', function(data) {
console.log('Game ended:', data);
showGameResults(data);
});
socket.on('player_left_game', function(data) {
console.log('Player left game:', data);
showNotification(`🚪 ${data.username} покинул игру`, 'warning');
updateLeaderboard();
});
}
// Функция показа уведомлений (ИСПРАВЛЕННАЯ - без рекурсии)
function showNotification(message, type = 'info') {
// Проверяем, есть ли уже глобальная функция showNotification
if (window.showNotification && window.showNotification !== showNotification) {
// Используем глобальную функцию если она существует и это не текущая функция
window.showNotification(message, type);
return;
}
// Создаем свое уведомление
const notification = document.createElement('div');
notification.className = 'game-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)';
if (type === 'info') notification.style.borderLeftColor = 'var(--accent-color)';
notification.textContent = message;
document.body.appendChild(notification);
// Автоматическое скрытие через 3 секунды
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 300);
}, 3000);
}
// Функции управления игрой
function loadGameData() {
fetch(`/api/game/${roomCode}/state`)
.then(response => response.json())
.then(data => {
if (data.success) {
currentPhase = data.phase;
phaseEndTime = data.phase_end;
updatePhaseDisplay();
setupPhaseTimer();
}
})
.catch(error => {
console.error('Error loading game data:', error);
showNotification('❌ Ошибка загрузки данных игры', 'error');
});
}
function updateGameState() {
fetch(`/api/game/${roomCode}/state`)
.then(response => response.json())
.then(data => {
if (data.success) {
// Обновляем капитал игрока
if (data.player_capital !== undefined) {
document.getElementById('player-capital-display').textContent =
formatCurrency(data.player_capital);
}
// Обновляем фазу если изменилась
if (data.phase !== currentPhase) {
currentPhase = data.phase;
phaseEndTime = data.phase_end;
updatePhaseDisplay();
setupPhaseTimer();
}
}
})
.catch(error => {
console.error('Error updating game state:', error);
showNotification('❌ Ошибка обновления состояния', 'error');
});
}
function setupPhaseTimer() {
if (!phaseEndTime) return;
const endTime = new Date(phaseEndTime);
const now = new Date();
const diffMs = endTime - now;
if (diffMs <= 0) {
document.getElementById('phase-timer').textContent = 'Время вышло';
return;
}
// Очищаем предыдущий таймер
if (gameTimer) clearInterval(gameTimer);
// Обновляем таймер каждую секунду
gameTimer = setInterval(() => {
const now = new Date();
const diffMs = endTime - now;
if (diffMs <= 0) {
clearInterval(gameTimer);
document.getElementById('phase-timer').textContent = 'Время вышло';
return;
}
const minutes = Math.floor(diffMs / 60000);
const seconds = Math.floor((diffMs % 60000) / 1000);
const timerElement = document.getElementById('phase-timer');
timerElement.textContent = `${getPhaseName(currentPhase)}: ${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
// Изменяем цвет при малом времени
if (minutes === 0 && seconds < 30) {
timerElement.classList.add('danger');
timerElement.classList.remove('warning');
} else if (minutes === 0 && seconds < 60) {
timerElement.classList.add('warning');
timerElement.classList.remove('danger');
} else {
timerElement.classList.remove('warning', 'danger');
}
}, 1000);
}
function getPhaseName(phase) {
const phases = {
'action': 'Действия',
'market': 'Реакция рынка',
'event': 'События',
'results': 'Итоги'
};
return phases[phase] || phase;
}
function updatePhaseDisplay() {
// Активируем соответствующий таб
const phaseTabs = {
'action': 'action-tab',
'market': 'market-tab',
'event': 'events-tab',
'results': 'results-tab'
};
const activeTabId = phaseTabs[currentPhase];
if (activeTabId) {
switchTab(activeTabId.split('-')[0]);
}
// Обновляем текст таймера
document.getElementById('phase-timer').textContent =
`${getPhaseName(currentPhase)}`;
// Показываем/скрываем кнопки в зависимости от фазы
const endTurnBtn = document.getElementById('end-turn-btn');
if (endTurnBtn) {
endTurnBtn.style.display = currentPhase === 'action' ? 'block' : 'none';
}
}
function updateCapitalProgress() {
const goal = 500000;
const progress = Math.min((playerCapital / goal) * 100, 100);
const progressBar = document.getElementById('capital-progress');
const progressText = document.getElementById('capital-progress-text');
if (progressBar) progressBar.style.width = progress + '%';
if (progressText) progressText.textContent = Math.round(progress) + '%';
}
// Функции для работы с активами
function loadAssets() {
fetch(`/api/game/${roomCode}/assets`)
.then(response => response.json())
.then(data => {
if (data.success && data.assets) {
displayAssets(data.assets);
}
})
.catch(error => {
console.error('Error loading assets:', error);
showNotification('❌ Ошибка загрузки активов', 'error');
});
}
function displayAssets(assets) {
const assetsList = document.getElementById('assets-list');
if (!assetsList) return;
assetsList.innerHTML = '';
Object.entries(assets).forEach(([assetId, assetData]) => {
const assetElement = document.createElement('div');
assetElement.className = 'asset-item';
assetElement.innerHTML = `
<div class="asset-info">
<div class="asset-name">
${getAssetName(assetId)}
</div>
<div class="asset-meta">
Волатильность: ${(assetData.volatility * 100).toFixed(1)}%
</div>
</div>
<div style="text-align: right;">
<div class="asset-price">
${formatCurrency(assetData.price)}
</div>
<button onclick="showBuyAssetModal('${assetId}', ${assetData.price})"
class="button" style="margin-top: 5px; padding: 5px 10px; font-size: 0.9rem;">
Купить
</button>
</div>
`;
assetsList.appendChild(assetElement);
});
}
function getAssetName(assetId) {
const names = {
'stock_gazprom': 'Акции Газпрома',
'real_estate': 'Недвижимость',
'bitcoin': 'Биткоин',
'oil': 'Нефть'
};
return names[assetId] || assetId;
}
function showBuyAssetModal(assetId, price) {
selectedAsset = { id: assetId, price: price };
document.getElementById('buy-asset-title').textContent = `Покупка ${getAssetName(assetId)}`;
document.getElementById('asset-price').textContent = formatCurrency(price);
document.getElementById('player-capital-display').textContent = formatCurrency(playerCapital);
const maxQuantity = Math.floor(playerCapital / price);
document.getElementById('max-quantity').textContent = `${maxQuantity} шт.`;
document.getElementById('buy-quantity').max = maxQuantity;
document.getElementById('buy-quantity').value = 1;
updateBuyCalculations();
document.getElementById('buy-asset-modal').classList.add('active');
}
function updateBuyCalculations() {
if (!selectedAsset) return;
const quantity = parseInt(document.getElementById('buy-quantity').value) || 1;
const price = selectedAsset.price;
const totalCost = quantity * price;
document.getElementById('total-cost').textContent = formatCurrency(totalCost);
document.getElementById('remaining-capital').textContent = formatCurrency(playerCapital - totalCost);
}
function confirmBuyAsset() {
if (!selectedAsset) return;
const quantity = parseInt(document.getElementById('buy-quantity').value) || 1;
const totalCost = quantity * selectedAsset.price;
if (totalCost > playerCapital) {
showNotification('❌ Недостаточно средств', 'error');
return;
}
fetch(`/api/game/${roomCode}/buy`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
},
body: JSON.stringify({
asset_id: selectedAsset.id,
quantity: quantity,
price: selectedAsset.price
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(`✅ Куплено ${quantity} ${getAssetName(selectedAsset.id)}`, 'success');
cancelBuyAsset();
// Обновляем капитал
playerCapital = data.new_capital;
updateCapitalProgress();
loadAssets(); // Перезагружаем активы
// Отправляем WebSocket событие
if (typeof socket !== 'undefined') {
socket.emit('player_bought_asset', {
room: roomCode,
player_id: playerId,
asset_id: selectedAsset.id,
quantity: quantity,
price: selectedAsset.price
});
}
} else {
showNotification('❌ Ошибка: ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error buying asset:', error);
showNotification('❌ Ошибка при покупке', 'error');
});
}
function cancelBuyAsset() {
selectedAsset = null;
document.getElementById('buy-asset-modal').classList.remove('active');
}
// Продажа активов
function sellAsset(assetId, quantity) {
if (!confirm(`Продать ${quantity} ${getAssetName(assetId)}?`)) {
return;
}
fetch(`/api/game/${roomCode}/sell`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
},
body: JSON.stringify({
asset_id: assetId,
quantity: quantity
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(`✅ Продано ${quantity} ${getAssetName(assetId)}`, 'success');
updateGameState();
} else {
showNotification('❌ Ошибка: ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error selling asset:', error);
showNotification('❌ Ошибка при продаже', 'error');
});
}
// Использование способности
function useAbility() {
fetch(`/api/game/${roomCode}/use_ability`, {
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(`✨ Использована способность: ${data.ability_name}`, 'success');
if (typeof socket !== 'undefined') {
socket.emit('player_used_ability', {
room: roomCode,
player_id: playerId,
ability: data.ability_name
});
}
} else {
showNotification('❌ Ошибка: ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error using ability:', error);
showNotification('❌ Ошибка при использовании способности', 'error');
});
}
// Завершение фазы действий
function endActionPhase() {
if (!confirm('Завершить ваши действия и перейти к следующей фазе?')) {
return;
}
fetch(`/api/game/${roomCode}/end_action_phase`, {
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');
} else {
showNotification('❌ Ошибка: ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error ending action phase:', error);
showNotification('❌ Ошибка при завершении фазы', 'error');
});
}
function skipTurn() {
if (confirm('Пропустить ход? Вы не сможете совершать действия в этой фазе.')) {
endActionPhase();
}
}
// Обновление лидерборда
function updateLeaderboard() {
fetch(`/api/game/${roomCode}/leaderboard`)
.then(response => response.json())
.then(data => {
if (data.success && data.players) {
displayLeaderboard(data.players);
}
})
.catch(error => {
console.error('Error loading leaderboard:', error);
showNotification('❌ Ошибка загрузки лидерборда', 'error');
});
}
function displayLeaderboard(players) {
const leaderboardElement = document.getElementById('leaderboard');
if (!leaderboardElement) return;
leaderboardElement.innerHTML = '';
players.sort((a, b) => b.capital - a.capital);
players.forEach((player, index) => {
const position = index + 1;
const isCurrentPlayer = player.user_id === {{ current_user.id }};
const playerElement = document.createElement('div');
playerElement.className = 'player-item';
playerElement.style.backgroundColor = isCurrentPlayer ? '#f0f8ff' : 'transparent';
playerElement.style.borderLeft = isCurrentPlayer ? '4px solid var(--primary-color)' : 'none';
playerElement.innerHTML = `
<div class="player-info">
<div class="player-avatar" style="background-color: ${getPositionColor(position)};">
${position}
</div>
<div>
<div style="display: flex; align-items: center; gap: 5px;">
<strong>${player.username}</strong>
${isCurrentPlayer ?
'<span style="background-color: #e3f2fd; color: #1976d2; padding: 2px 6px; border-radius: 10px; font-size: 0.75rem;">Вы</span>' :
''}
</div>
<div style="font-size: 0.9rem; color: var(--light-text);">
${getAbilityName(player.ability)}
</div>
</div>
</div>
<div class="player-capital">
${formatCurrency(player.capital)}
</div>
`;
leaderboardElement.appendChild(playerElement);
});
}
function getPositionColor(position) {
if (position === 1) return '#ffd700'; // Золотой
if (position === 2) return '#c0c0c0'; // Серебряный
if (position === 3) return '#cd7f32'; // Бронзовый
return 'var(--primary-color)';
}
function getAbilityName(abilityCode) {
const abilities = {
'crisis_investor': 'Кризисный инвестор',
'lobbyist': 'Лоббист',
'predictor': 'Предсказатель',
'golden_pillow': 'Золотая подушка',
'shadow_accountant': 'Теневая бухгалтерия',
'credit_magnate': 'Кредитный магнат',
'bear_raid': 'Медвежий набег',
'fake_news': 'Фейковые новости',
'dividend_king': 'Король дивидендов',
'raider_capture': 'Рейдерский захват',
'mafia_connections': 'Мафиозные связи',
'economic_advisor': 'Экономический советник',
'currency_speculator': 'Валютный спекулянт'
};
return abilities[abilityCode] || abilityCode;
}
// Работа с чатом игры
function loadGameChat() {
// Загружаем историю чата
const chatContainer = document.getElementById('game-chat-messages');
chatContainer.innerHTML = '<div style="text-align: center; color: var(--light-text); padding: 20px;">Нет сообщений</div>';
}
function sendGameMessage() {
const input = document.getElementById('game-chat-input');
const message = input.value.trim();
if (!message) return;
if (typeof socket !== 'undefined') {
socket.emit('game_chat_message', {
room: roomCode,
message: message,
username: '{{ current_user.username }}',
user_id: {{ current_user.id }},
timestamp: new Date().toISOString()
});
}
addGameChatMessage({
username: '{{ current_user.username }} (Вы)',
user_id: {{ current_user.id }},
message: message,
timestamp: new Date().toISOString()
});
input.value = '';
input.focus();
}
function addGameChatMessage(data) {
const chatContainer = document.getElementById('game-chat-messages');
if (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 == {{ current_user.id }} ? '#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 confirmExitGame() {
showExitModal();
return false;
}
function confirmExitGameAction() {
// Закрываем модальное окно
document.getElementById('exit-game-modal').classList.remove('active');
// Выполняем выход
performExitGame();
}
function cancelExitGame() {
document.getElementById('exit-game-modal').classList.remove('active');
}
function showExitModal(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
document.getElementById('exit-game-modal').classList.add('active');
}
function performExitGame() {
// Отправляем WebSocket событие о выходе если подключены
if (typeof socket !== 'undefined' && socket.connected && roomCode) {
socket.emit('leave_game_room', {
room: roomCode,
reason: 'exited'
});
}
// Редирект в лобби
showNotification('🔄 Перенаправление в лобби...', 'info');
setTimeout(() => {
window.location.href = '/room/' + roomCode;
}, 1000);
}
// Вспомогательные функции
function formatCurrency(amount) {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0
}).format(amount);
}
// Обновляем поле количества при изменении
document.getElementById('buy-quantity')?.addEventListener('input', updateBuyCalculations);
// Закрытие модальных окон
document.addEventListener('click', function(event) {
if (event.target.classList.contains('modal-backdrop')) {
document.getElementById('exit-game-modal').classList.remove('active');
document.getElementById('buy-asset-modal').classList.remove('active');
}
});
// Обработка клавиши Escape
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
document.getElementById('exit-game-modal').classList.remove('active');
document.getElementById('buy-asset-modal').classList.remove('active');
}
});
</script>
<style>
/* Дополнительные стили для игры */
.player-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 1.1rem;
flex-shrink: 0;
}
.timer {
background-color: white;
padding: 5px 15px;
border-radius: 20px;
font-weight: bold;
font-size: 0.9rem;
color: var(--primary-color);
border: 2px solid var(--primary-color);
}
.timer.warning {
color: var(--warning-color);
border-color: var(--warning-color);
animation: pulse 1s infinite;
}
.timer.danger {
color: var(--danger-color);
border-color: var(--danger-color);
animation: pulse 0.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
.asset-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #eee;
transition: var(--transition);
}
.asset-item:hover {
background-color: #f9f9f9;
border-radius: var(--border-radius);
padding: 12px;
margin: 0 -15px;
}
.asset-name {
font-weight: 600;
font-size: 1rem;
}
.asset-meta {
font-size: 0.85rem;
color: var(--light-text);
margin-top: 3px;
}
.asset-price {
font-weight: bold;
color: var(--success-color);
font-size: 1.1rem;
}
.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: 10000;
}
.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);
}
}
.game-notification {
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;
}
.game-notification.success {
border-left-color: var(--success-color);
}
.game-notification.error {
border-left-color: var(--danger-color);
}
.game-notification.warning {
border-left-color: var(--warning-color);
}
.game-notification.info {
border-left-color: var(--accent-color);
}
/* Адаптивные стили для игры */
@media (max-width: 480px) {
.header {
flex-wrap: wrap;
height: auto;
min-height: 60px;
padding: 10px;
}
.timer {
font-size: 0.8rem;
padding: 3px 10px;
margin-top: 5px;
}
.tab-container {
flex-wrap: wrap;
}
.tab {
flex: 1 0 50%;
font-size: 0.9rem;
padding: 10px 5px;
}
}
</style>
{% endblock %}