1275 lines
46 KiB
HTML
1275 lines
46 KiB
HTML
{% 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 %} |