Files
cm/templates/game.html
2026-04-20 14:39:59 +03:00

2966 lines
118 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Игра: {{ room.name }} - Капитал & Рынок{% endblock %}
{% block content %}
<div class="screen active">
<!-- В шапке игры -->
<div class="header">
<a href="#" 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">
Фаза действий
</div>
<div id="month-display" 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);">
У вас есть <span id="header-capital">{{ player.capital|format_currency }}</span> для инвестиций.
</p>
<!-- Кнопка открытия модального окна с активами -->
<button onclick="openAssetsModal()" class="button"
style="width: 100%; padding: 15px; font-size: 1.1rem;">
📊 Открыть биржу активов
</button>
<!-- Краткая статистика -->
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px; padding: 10px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="text-align: center;">
<div style="font-size: 0.9rem; color: var(--light-text);">Всего активов</div>
<div style="font-size: 1.3rem; font-weight: bold;" id="total-assets-count">0</div>
</div>
<div style="text-align: center;">
<div style="font-size: 0.9rem; color: var(--light-text);">Диверсификация</div>
<div style="font-size: 1.3rem; font-weight: bold;" id="diversity-count">0</div>
</div>
</div>
</div>
<!-- Фаза действий -->
<div class="card">
<h3>💼 Инвестиционные возможности</h3>
<p style="margin-bottom: 15px; color: var(--light-text);">
У вас есть {{ player.capital|format_currency }} для инвестиций.
Выберите активы для покупки или продажи.
</p>
<!-- Кнопка завершения хода -->
<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>
<!-- Модальное окно продажи актива -->
<div id="sell-asset-modal" class="modal-backdrop">
<div class="modal">
<h3 id="sell-asset-title">Продажа актива</h3>
<div style="margin: 20px 0;">
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>Текущая цена:</span>
<strong id="sell-asset-price">0 ₽</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>У вас в наличии:</span>
<strong id="sell-player-quantity">0 шт.</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<span>Комиссия:</span>
<strong id="sell-fee">1%</strong>
</div>
<div class="input-group">
<label for="sell-quantity">Количество для продажи</label>
<input type="number" id="sell-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="sell-revenue">0 ₽</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 5px;">
<span>Комиссия:</span>
<strong id="sell-fee-amount">0 ₽</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 5px;">
<span>К получению:</span>
<strong id="sell-total">0 ₽</strong>
</div>
</div>
</div>
<div class="flex gap-2">
<button onclick="confirmSellAsset()" class="button warning" style="flex: 1;">
Продать
</button>
<button onclick="cancelSellAsset()" class="button secondary" style="flex: 1;">
Отмена
</button>
</div>
</div>
</div>
<!-- Модальное окно биржи активов -->
<div id="assets-modal" class="modal-backdrop">
<div class="modal" style="max-width: 900px; width: 95%; max-height: 90vh; overflow-y: auto;">
<!-- Заголовок модального окна -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; position: sticky; top: 0; background: white; padding: 15px 0; border-bottom: 2px solid var(--primary-color); z-index: 10;">
<h2 style="margin: 0; display: flex; align-items: center; gap: 10px;">
<span style="font-size: 1.8rem;">📈</span>
Биржа активов
</h2>
<div style="display: flex; align-items: center; gap: 20px;">
<div style="text-align: right;">
<div style="font-size: 0.9rem; color: var(--light-text);">Ваш капитал</div>
<div style="font-size: 1.5rem; font-weight: bold; color: var(--success-color);" id="modal-capital">
{{ player.capital|format_currency }}
</div>
</div>
<button onclick="closeAssetsModal()" class="button secondary" style="padding: 10px 20px;">
✕ Закрыть
</button>
</div>
</div>
<!-- Фильтры и поиск -->
<div style="margin-bottom: 20px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="display: flex; gap: 15px; flex-wrap: wrap; align-items: center;">
<div style="flex: 1; min-width: 200px;">
<input type="text" id="asset-search" placeholder="🔍 Поиск активов..."
style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: var(--border-radius);"
onkeyup="filterAssets()">
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<select id="category-filter" onchange="filterAssets()"
style="padding: 12px; border: 1px solid #ddd; border-radius: var(--border-radius); background: white;">
<option value="all">Все категории</option>
<option value="bonds">🏦 Облигации</option>
<option value="stocks">📈 Акции</option>
<option value="real_estate">🏠 Недвижимость</option>
<option value="business">🏢 Бизнес</option>
<option value="commodities">⛽ Сырье</option>
<option value="crypto">💰 Криптовалюта</option>
<option value="unique">🏆 Уникальные</option>
</select>
<select id="sort-filter" onchange="filterAssets()"
style="padding: 12px; border: 1px solid #ddd; border-radius: var(--border-radius); background: white;">
<option value="default">По умолчанию</option>
<option value="price_asc">Цена (по возрастанию)</option>
<option value="price_desc">Цена (по убыванию)</option>
<option value="volatility_asc">Волатильность (низкая)</option>
<option value="income_desc">Доходность (высокая)</option>
</select>
</div>
</div>
<!-- Дополнительные фильтры -->
<div style="display: flex; gap: 15px; margin-top: 15px; flex-wrap: wrap;">
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" id="show-only-available" onchange="filterAssets()" checked>
<span>Только доступные</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" id="show-only-affordable" onchange="filterAssets()" checked>
<span>Только по средствам</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" id="show-only-income" onchange="filterAssets()">
<span>Только с доходностью</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" id="show-my-assets" onchange="filterAssets()">
<span>Мои активы</span>
</label>
</div>
</div>
<!-- Список активов -->
<div id="assets-list" style="min-height: 400px;">
<div style="text-align: center; padding: 40px; color: var(--light-text);">
<div style="font-size: 3rem; margin-bottom: 20px;">📊</div>
<p>Загрузка активов...</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Глобальные переменные
const roomCode = '{{ room.code }}';
const playerId = {{ player.id }};
let playerCapital = {{ player.capital }};
let currentPhase = '{{ game_state.phase if game_state and game_state.phase else "action" }}';
let phaseEndTime = '{{ game_state.phase_end if game_state and game_state.phase_end else "" }}';
let selectedAsset = null;
let gameTimer = null;
let currentMonth = {{ room.current_month }};
let totalMonths = {{ room.total_months }};
let isInitialized = false;
let initRetryCount = 0;
const MAX_RETRIES = 20;
// Функция ожидания элемента
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkElement = () => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
} else if (Date.now() - startTime > timeout) {
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
} else {
setTimeout(checkElement, 100);
}
};
checkElement();
});
}
// Функция инициализации игры
async function initializeGame() {
console.log('Initializing game... (attempt ' + (initRetryCount + 1) + ')');
try {
// Ждем критически важные элементы
const timerElement = await waitForElement('#phase-timer', 3000);
const monthElement = await waitForElement('#month-display', 3000);
console.log('✅ DOM elements found:', {
timer: timerElement?.id,
month: monthElement?.id
});
// Подключаемся к WebSocket
if (typeof socket !== 'undefined' && roomCode) {
socket.emit('join_game_room', { room: roomCode });
console.log('✅ Joined game room via WebSocket');
}
// Инициализация табов
if (typeof initTabs === 'function') {
initTabs();
}
// Загрузка данных игры
await loadGameData();
// Обновление прогресс-бара капитала
updateCapitalProgress();
// Настройка таймера фазы
setupPhaseTimer();
// Загрузка активов для покупки
loadAssets();
// Загрузка лидерборда
updateLeaderboard();
// Загрузка истории чата
loadGameChat();
// Автоматическое обновление каждые 10 секунд
setInterval(updateGameState, 10000);
isInitialized = true;
console.log('🎮 Game initialized successfully!');
} catch (error) {
console.error('❌ Failed to initialize game:', error);
initRetryCount++;
if (initRetryCount < MAX_RETRIES) {
console.log(`Retrying initialization (${initRetryCount}/${MAX_RETRIES})...`);
setTimeout(initializeGame, 1000);
} else {
console.error('❌ Max retries reached. Game initialization failed.');
showNotification('❌ Ошибка загрузки игры. Обновите страницу.', 'error');
}
}
}
// Запускаем инициализацию
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
console.log('📄 DOM Content Loaded');
initializeGame();
});
} else {
console.log('📄 DOM already loaded');
initializeGame();
}
// 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;
// Обновляем месяц если есть
if (data.month) {
currentMonth = data.month;
console.log(`Month updated to ${currentMonth}/${totalMonths}`);
}
if (data.total_months) {
totalMonths = data.total_months;
}
// Обновляем интерфейс
updatePhaseDisplay();
setupPhaseTimer();
// Обновляем отображение месяца
const monthElement = document.getElementById('month-display');
if (monthElement) {
monthElement.textContent = `Месяц ${currentMonth}/${totalMonths}`;
}
// Обновляем заголовок в карточке действий
const actionTitle = document.querySelector('#action-tab .card h3');
if (actionTitle && currentPhase === 'action') {
actionTitle.textContent = `💼 Инвестиционные возможности (Месяц ${currentMonth}/${totalMonths})`;
}
showNotification(`📢 ${data.message || `Новая фаза: ${getPhaseName(data.phase)}`}`, 'info');
});
socket.on('market_updated', function(data) {
console.log('Market updated:', data);
updateMarketDisplay(data.assets);
showNotification('📊 Рынок обновлен!', 'info', 2000);
});
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_joined_game', function(data) {
console.log('Player joined game:', data);
showNotification(`🎮 ${data.username} присоединился к игре`, 'info');
updateLeaderboard();
});
socket.on('player_left_game', function(data) {
console.log('Player left game:', data);
showNotification(`🚪 ${data.username} покинул игру`, 'warning');
updateLeaderboard();
});
socket.on('game_event', function(data) {
console.log('Game event:', data);
showGameEvent(data);
// Автоматически переключаемся на вкладку событий
setTimeout(() => {
switchTab('events');
}, 1000);
});
socket.on('player_capital_updated', function(data) {
console.log('Player capital updated:', data);
// Обновляем капитал текущего игрока
if (data.user_id === {{ current_user.id }}) {
playerCapital = data.capital;
updateCapitalDisplay();
// Показываем уведомление о прибыли если есть
if (data.profit) {
const profitText = data.profit > 0 ? `+${formatCurrency(data.profit)}` : formatCurrency(data.profit);
const profitClass = data.profit > 0 ? 'success' : 'danger';
showNotification(`💰 Ваш капитал: ${formatCurrency(data.capital)} (${profitText})`, profitClass, 3000);
}
}
});
socket.on('income_distributed', function(data) {
console.log('Income distributed:', data);
showNotification(data.message, 'success', 4000);
// Обновляем данные игры чтобы получить новый капитал
setTimeout(() => {
updateGameState();
loadAssets(); // Перезагружаем активы для обновления цен
}, 1000);
});
socket.on('leaderboard_updated', function(data) {
console.log('Leaderboard updated:', data);
// Обновляем месяц если есть
if (data.month) {
currentMonth = data.month;
const monthElement = document.getElementById('month-display');
if (monthElement) {
monthElement.textContent = `Месяц ${currentMonth}/${totalMonths}`;
}
}
// Обновляем лидерборд
if (data.players) {
displayLeaderboard(data.players);
}
});
socket.on('month_updated', function(data) {
console.log('Month updated:', data);
if (data.month) {
currentMonth = data.month;
const monthElement = document.getElementById('month-display');
if (monthElement) {
monthElement.textContent = `Месяц ${currentMonth}/${totalMonths}`;
console.log(`Month display updated via month_updated: ${currentMonth}/${totalMonths}`);
}
showNotification(`📅 Месяц ${currentMonth}/${totalMonths}`, 'info', 2000);
}
});
}
// Функция показа уведомлений (ИСПРАВЛЕННАЯ - без рекурсии)
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() {
console.log('Loading game data...');
fetch(`/api/game/${roomCode}/state`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
console.log('Game data loaded:', data);
// Обновляем фазу
if (data.phase) {
currentPhase = data.phase;
}
// Обновляем время окончания фазы
if (data.phase_end) {
phaseEndTime = data.phase_end;
}
// Обновляем месяц - ВАЖНО!
if (data.current_month) {
currentMonth = data.current_month;
console.log(`Month set to ${currentMonth} from API`);
}
// Обновляем общее количество месяцев
if (data.total_months) {
totalMonths = data.total_months;
}
// Обновляем капитал игрока
if (data.player_capital) {
playerCapital = data.player_capital;
updateCapitalDisplay();
}
// Принудительно обновляем отображение месяца
const monthElement = document.getElementById('month-display');
if (monthElement) {
monthElement.textContent = `Месяц ${currentMonth}/${totalMonths}`;
console.log(`Month display set to: ${currentMonth}/${totalMonths}`);
}
// Обновляем интерфейс
updatePhaseDisplay();
setupPhaseTimer();
}
})
.catch(error => {
console.error('Error loading game data:', error);
setTimeout(loadGameData, 3000);
});
}
function updateGameState() {
fetch(`/api/game/${roomCode}/state`)
.then(response => response.json())
.then(data => {
if (data.success) {
// Обновляем капитал игрока
if (data.player_capital !== undefined) {
const oldCapital = playerCapital;
playerCapital = data.player_capital;
// Если капитал изменился, показываем уведомление
if (oldCapital !== playerCapital) {
const profit = playerCapital - oldCapital;
if (profit > 0) {
showNotification(`💰 +${formatCurrency(profit)} к капиталу!`, 'success', 3000);
}
updateCapitalDisplay();
}
}
// Обновляем фазу если изменилась
if (data.phase !== currentPhase) {
currentPhase = data.phase;
phaseEndTime = data.phase_end;
updatePhaseDisplay();
setupPhaseTimer();
}
// Обновляем месяц
if (data.current_month && data.current_month !== currentMonth) {
currentMonth = data.current_month;
const monthElement = document.getElementById('month-display');
if (monthElement) {
monthElement.textContent = `Месяц ${currentMonth}/${totalMonths}`;
}
}
}
})
.catch(error => {
console.error('Error updating game state:', error);
});
}
function setupPhaseTimer() {
const timerElement = document.getElementById('phase-timer');
if (!timerElement) {
console.warn('Timer element not found, creating...');
const headerRight = document.querySelector('.header div:last-child');
if (headerRight) {
const newTimer = document.createElement('div');
newTimer.id = 'phase-timer';
newTimer.className = 'timer';
newTimer.textContent = `${getPhaseName(currentPhase)} | М:${currentMonth}/${totalMonths}`;
headerRight.insertBefore(newTimer, headerRight.firstChild);
}
setTimeout(setupPhaseTimer, 500);
return;
}
// Всегда показываем фазу и месяц
timerElement.textContent = `${getPhaseName(currentPhase)} | М:${currentMonth}/${totalMonths}`;
// Если нет времени окончания - выходим
if (!phaseEndTime || phaseEndTime === 'None' || phaseEndTime === '') {
console.log('No phase end time, showing static phase name');
return;
}
try {
// ВАЖНО: Добавляем 'Z' чтобы интерпретировать как UTC
let endTimeStr = phaseEndTime;
if (!endTimeStr.endsWith('Z') && !endTimeStr.includes('+')) {
endTimeStr += 'Z'; // Добавляем UTC маркер
}
const endTime = new Date(endTimeStr);
console.log('⏰ End time (UTC):', endTimeStr);
console.log('⏰ End time (parsed):', endTime);
console.log('⏰ Current time (local):', new Date());
console.log('⏰ Difference (ms):', endTime - new Date());
// Проверяем валидность даты
if (isNaN(endTime.getTime())) {
console.error('Invalid end time:', phaseEndTime);
return;
}
// Очищаем старый таймер
if (gameTimer) {
clearInterval(gameTimer);
gameTimer = null;
}
// Функция обновления таймера
function updateTimer() {
const now = new Date();
const diffMs = endTime - now;
if (diffMs <= 0) {
timerElement.textContent = `${getPhaseName(currentPhase)}: 0:00 | М:${currentMonth}/${totalMonths}`;
timerElement.classList.add('danger');
return false;
}
const minutes = Math.floor(diffMs / 60000);
const seconds = Math.floor((diffMs % 60000) / 1000);
timerElement.textContent = `${getPhaseName(currentPhase)}: ${minutes}:${seconds < 10 ? '0' : ''}${seconds} | М:${currentMonth}/${totalMonths}`;
// Изменяем цвет при малом времени
timerElement.classList.remove('warning', 'danger');
if (minutes === 0 && seconds < 30) {
timerElement.classList.add('danger');
} else if (minutes === 0 && seconds < 60) {
timerElement.classList.add('warning');
}
return true;
}
// Первое обновление
updateTimer();
// Запускаем интервал
gameTimer = setInterval(updateTimer, 1000);
console.log('✅ Timer started, ends at:', endTime.toLocaleString());
} catch (error) {
console.error('❌ Timer error:', error);
}
}
function getPhaseName(phase) {
const phases = {
'action': 'Действия',
'market': 'Реакция рынка',
'event': 'События',
'results': 'Итоги'
};
return phases[phase] || phase;
}
function updatePhaseDisplay() {
// Обновляем текст таймера
const timerElement = document.getElementById('phase-timer');
if (timerElement) {
timerElement.textContent = `${getPhaseName(currentPhase)}`;
}
// Обновляем отображение месяца - ИСПРАВЛЕНО
const monthElement = document.getElementById('month-display');
if (monthElement) {
monthElement.textContent = `Месяц ${currentMonth}/${totalMonths}`;
console.log(`Month display updated: ${currentMonth}/${totalMonths}`);
} else {
console.warn('Month display element not found');
}
// Активируем соответствующий таб
const phaseTabs = {
'action': 'action-tab',
'market': 'market-tab',
'event': 'events-tab',
'results': 'results-tab',
'waiting': 'action-tab'
};
const activeTabId = phaseTabs[currentPhase];
if (activeTabId) {
// Деактивируем все табы
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
// Активируем нужный таб
const tabContent = document.getElementById(activeTabId);
if (tabContent) {
tabContent.classList.add('active');
}
const tabButton = document.querySelector(`.tab[data-tab="${activeTabId.split('-')[0]}"]`);
if (tabButton) {
tabButton.classList.add('active');
}
}
// Показываем/скрываем кнопки в зависимости от фазы
const endTurnBtn = document.getElementById('end-turn-btn');
if (endTurnBtn) {
endTurnBtn.style.display = currentPhase === 'action' ? 'block' : 'none';
}
// Обновляем заголовок в карточке действий
const actionTitle = document.querySelector('#action-tab .card h3');
if (actionTitle && currentPhase === 'action') {
actionTitle.textContent = `💼 Инвестиционные возможности (Месяц ${currentMonth}/${totalMonths})`;
}
}
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) {
window.allAssetsData = data.assets;
displayAssets(data.assets, data.categories);
}
})
.catch(error => {
console.error('Error loading assets:', error);
showNotification('❌ Ошибка загрузки активов', 'error');
});
}
function displayAssets(assets, categories) {
const assetsList = document.getElementById('assets-list');
if (!assetsList) return;
assetsList.innerHTML = '';
// Сохраняем категории глобально для использования в других функциях
window.assetCategories = categories || {
'bonds': {'name': 'Облигации', 'icon': '🏦'},
'stocks': {'name': 'Акции', 'icon': '📈'},
'real_estate': {'name': 'Недвижимость', 'icon': '🏠'},
'crypto': {'name': 'Криптовалюта', 'icon': '💰'},
'commodities': {'name': 'Сырьевые товары', 'icon': '⛽'},
'business': {'name': 'Бизнес', 'icon': '🏢'},
'unique': {'name': 'Уникальные активы', 'icon': '🏆'}
};
// Группируем активы по категориям
const groupedAssets = {};
Object.entries(assets).forEach(([assetId, assetData]) => {
const category = assetData.category || 'other';
if (!groupedAssets[category]) {
groupedAssets[category] = [];
}
groupedAssets[category].push({ id: assetId, ...assetData });
});
// Порядок категорий для отображения
const categoryOrder = ['bonds', 'stocks', 'real_estate', 'business', 'commodities', 'crypto', 'unique'];
// Отображаем активы по категориям в заданном порядке
categoryOrder.forEach(categoryKey => {
if (groupedAssets[categoryKey] && groupedAssets[categoryKey].length > 0) {
const category = window.assetCategories[categoryKey] || { name: categoryKey, icon: '📊' };
// Заголовок категории
const categoryHeader = document.createElement('div');
categoryHeader.className = 'category-header';
categoryHeader.innerHTML = `
<span style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.3rem;">${category.icon || '📊'}</span>
<span style="font-weight: 600; color: var(--primary-color);">${category.name || categoryKey}</span>
</span>
`;
assetsList.appendChild(categoryHeader);
// Сортируем активы внутри категории по цене (от дешевых к дорогим)
groupedAssets[categoryKey].sort((a, b) => a.price - b.price);
// Активы категории
groupedAssets[categoryKey].forEach(asset => {
const assetElement = createAssetElement(asset);
assetsList.appendChild(assetElement);
});
}
});
// Показываем оставшиеся категории, которые не вошли в порядок
Object.keys(groupedAssets).forEach(categoryKey => {
if (!categoryOrder.includes(categoryKey) && groupedAssets[categoryKey].length > 0) {
const category = window.assetCategories[categoryKey] || { name: categoryKey, icon: '📊' };
const categoryHeader = document.createElement('div');
categoryHeader.className = 'category-header';
categoryHeader.innerHTML = `
<span style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.3rem;">${category.icon || '📊'}</span>
<span style="font-weight: 600; color: var(--primary-color);">${category.name || categoryKey}</span>
</span>
`;
assetsList.appendChild(categoryHeader);
groupedAssets[categoryKey].forEach(asset => {
const assetElement = createAssetElement(asset);
assetsList.appendChild(assetElement);
});
}
});
}
function createAssetElement(asset) {
const assetElement = document.createElement('div');
assetElement.className = 'asset-item';
const isAvailable = asset.available === null || asset.available > 0;
const canAfford = window.playerCapital >= asset.price * asset.min_purchase;
const canBuyMore = !asset.max_per_player || asset.player_quantity < asset.max_per_player;
let availabilityText = '';
if (asset.available !== null) {
const percentage = ((asset.available / asset.total) * 100).toFixed(0);
availabilityText = `<div style="font-size: 0.8rem; color: var(--light-text); margin-top: 3px;">
Доступно: <span style="font-weight: 600; color: ${percentage > 30 ? 'var(--success-color)' : 'var(--warning-color)'}">
${asset.available} / ${asset.total}
</span> (${percentage}%)
</div>`;
}
let playerQuantityText = '';
if (asset.player_quantity > 0) {
playerQuantityText = `<div style="font-size: 0.8rem; color: var(--success-color); margin-top: 3px;">
У вас: <span style="font-weight: 600;">${asset.player_quantity} шт.</span>
</div>`;
}
let limitText = '';
if (asset.max_per_player) {
limitText = `<div style="font-size: 0.8rem; color: var(--light-text); margin-top: 3px;">
Лимит: ${asset.player_quantity} / ${asset.max_per_player}
</div>`;
}
const volatilityLevel = getVolatilityLevel(asset.volatility);
const volatilityColors = {
'low': '#4CAF50',
'medium': '#FF9800',
'high': '#F44336'
};
assetElement.innerHTML = `
<div class="asset-info" style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 5px;">
<span style="font-size: 1.2rem;">${getAssetIcon(asset.category)}</span>
<span class="asset-name" style="font-size: 1.1rem; font-weight: 600;">${asset.name}</span>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 5px;">
<span style="font-size: 0.8rem; padding: 3px 8px; border-radius: 12px; background-color: ${volatilityColors[volatilityLevel]}20; color: ${volatilityColors[volatilityLevel]}; font-weight: 600;">
⚡ Волатильность: ${(asset.volatility * 100).toFixed(0)}%
</span>
${asset.income_per_month > 0 ?
`<span style="font-size: 0.8rem; padding: 3px 8px; border-radius: 12px; background-color: var(--success-color)20; color: var(--success-color); font-weight: 600;">
💰 Доход: +${(asset.income_per_month * 100).toFixed(1)}%/мес
</span>` : ''
}
</div>
${availabilityText}
${playerQuantityText}
${limitText}
<div style="font-size: 0.85rem; color: var(--light-text); margin-top: 8px; padding-top: 8px; border-top: 1px solid #eee;">
${asset.description || 'Нет описания'}
</div>
</div>
<div style="text-align: right; min-width: 120px;">
<div style="font-size: 1.3rem; font-weight: 700; color: var(--primary-color); margin-bottom: 10px;">
${formatCurrency(asset.price)}
</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
${asset.player_quantity > 0 ?
`<button onclick="showSellModal('${asset.id}', ${asset.price}, '${asset.name}', ${asset.player_quantity})"
class="button danger" style="padding: 8px 12px; font-size: 0.9rem; width: 100%;">
📉 Продать
</button>` : ''
}
${isAvailable && canAfford && canBuyMore ?
`<button onclick="showBuyAssetModal('${asset.id}', ${asset.price}, '${asset.name}', ${asset.min_purchase || 1}, ${asset.available || 'null'})"
class="button success" style="padding: 8px 12px; font-size: 0.9rem; width: 100%;">
📈 Купить
</button>` :
`<button disabled class="button" style="padding: 8px 12px; font-size: 0.9rem; width: 100%; opacity: 0.5; cursor: not-allowed;">
${!isAvailable ? 'Нет в наличии' : !canAfford ? 'Не хватает средств' : 'Лимит исчерпан'}
</button>`
}
</div>
</div>
`;
return assetElement;
}
function getAssetIcon(category) {
const icons = {
'bonds': '🏦',
'stocks': '📈',
'real_estate': '🏠',
'crypto': '💰',
'commodities': '⛽',
'business': '🏢',
'unique': '🏆',
'other': '📊'
};
return icons[category] || '📊';
}
function getVolatilityLevel(volatility) {
if (volatility < 0.15) return 'low';
if (volatility < 0.25) return 'medium';
return 'high';
}
function showBuyAssetModal(assetId, price, assetName, minQuantity, available) {
selectedAsset = {
id: assetId,
price: price,
name: assetName,
minQuantity: minQuantity,
available: available
};
document.getElementById('buy-asset-title').textContent = `Покупка: ${assetName}`;
document.getElementById('asset-price').textContent = formatCurrency(price);
document.getElementById('player-capital-display').textContent = formatCurrency(playerCapital);
const maxByCapital = Math.floor(playerCapital / price);
let maxQuantity = maxByCapital;
// Учитываем доступность
if (available !== null && available !== undefined) {
maxQuantity = Math.min(maxQuantity, available);
}
// Учитываем лимит на игрока
const assetData = allAssetsData?.[assetId];
if (assetData?.max_per_player) {
const currentQuantity = assetData.player_quantity || 0;
const remainingLimit = assetData.max_per_player - currentQuantity;
maxQuantity = Math.min(maxQuantity, remainingLimit);
}
document.getElementById('max-quantity').textContent = `${maxQuantity} шт.`;
document.getElementById('buy-quantity').max = maxQuantity;
document.getElementById('buy-quantity').value = Math.min(minQuantity, maxQuantity);
document.getElementById('buy-quantity').min = minQuantity;
document.getElementById('buy-quantity').step = minQuantity;
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 showSellModal(assetId, price, assetName, maxQuantity) {
selectedAssetForSell = {
id: assetId,
price: price,
name: assetName,
maxQuantity: maxQuantity
};
document.getElementById('sell-asset-title').textContent = `Продажа: ${assetName}`;
document.getElementById('sell-asset-price').textContent = formatCurrency(price);
document.getElementById('sell-player-quantity').textContent = `${maxQuantity} шт.`;
const feeRate = 0.01; // 1% комиссия
document.getElementById('sell-fee').textContent = `${feeRate * 100}%`;
document.getElementById('sell-quantity').max = maxQuantity;
document.getElementById('sell-quantity').value = 1;
document.getElementById('sell-quantity').min = 1;
updateSellCalculations();
document.getElementById('sell-asset-modal').classList.add('active');
}
function updateSellCalculations() {
if (!selectedAssetForSell) return;
const quantity = parseInt(document.getElementById('sell-quantity').value) || 1;
const price = selectedAssetForSell.price;
const feeRate = 0.01;
const revenue = quantity * price;
const fee = revenue * feeRate;
const total = revenue - fee;
document.getElementById('sell-revenue').textContent = formatCurrency(revenue);
document.getElementById('sell-fee-amount').textContent = formatCurrency(fee);
document.getElementById('sell-total').textContent = formatCurrency(total);
}
function cancelSellAsset() {
selectedAssetForSell = null;
document.getElementById('sell-asset-modal').classList.remove('active');
}
function confirmSellAsset() {
if (!selectedAssetForSell) return;
const quantity = parseInt(document.getElementById('sell-quantity').value) || 1;
if (quantity > selectedAssetForSell.maxQuantity) {
showNotification(`У вас только ${selectedAssetForSell.maxQuantity} шт.`, 'error');
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: selectedAssetForSell.id,
quantity: quantity
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, 'success');
cancelSellAsset();
// Обновляем капитал
playerCapital = data.new_capital;
updateCapitalProgress();
// Перезагружаем активы
loadAssets();
// Обновляем отображение капитала
document.getElementById('player-capital-display').textContent =
formatCurrency(playerCapital);
} else {
showNotification('❌ ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error selling asset:', error);
showNotification('❌ Ошибка при продаже', 'error');
});
}
// Добавляем обработчик input для поля количества
document.getElementById('sell-quantity')?.addEventListener('input', updateSellCalculations);
// Завершение фазы действий
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);
}
// Обновляем поле количества при изменении
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');
}
});
function openAssetsModal() {
document.getElementById('assets-modal').classList.add('active');
document.body.style.overflow = 'hidden'; // Запрещаем скролл body
loadAssets(); // Перезагружаем актуальные данные
}
function closeAssetsModal() {
document.getElementById('assets-modal').classList.remove('active');
document.body.style.overflow = ''; // Возвращаем скролл
}
// Обновляем функцию loadAssets
function loadAssets() {
fetch(`/api/game/${roomCode}/assets`)
.then(response => response.json())
.then(data => {
if (data.success && data.assets) {
allAssetsData = data.assets;
window.assetCategories = data.categories;
// Обновляем капитал из ответа сервера
if (data.player_capital) {
currentCapital = data.player_capital;
updateCapitalDisplay();
}
filterAssets(); // Применяем фильтры и отображаем
updateAssetStats(); // Обновляем статистику
}
})
.catch(error => {
console.error('Error loading assets:', error);
showNotification('❌ Ошибка загрузки активов', 'error');
});
}
// Функция фильтрации активов
function filterAssets() {
const searchText = document.getElementById('asset-search')?.value.toLowerCase() || '';
const category = document.getElementById('category-filter')?.value || 'all';
const sortBy = document.getElementById('sort-filter')?.value || 'default';
const showOnlyAvailable = document.getElementById('show-only-available')?.checked || false;
const showOnlyAffordable = document.getElementById('show-only-affordable')?.checked || false;
const showOnlyIncome = document.getElementById('show-only-income')?.checked || false;
const showMyAssets = document.getElementById('show-my-assets')?.checked || false;
filteredAssetsData = {};
Object.entries(allAssetsData).forEach(([assetId, assetData]) => {
// Фильтр по категории
if (category !== 'all' && assetData.category !== category) return;
// Фильтр по поиску
if (searchText && !assetData.name.toLowerCase().includes(searchText)) return;
// Фильтр по доступности
if (showOnlyAvailable && assetData.available !== null && assetData.available <= 0) return;
// Фильтр по средствам
if (showOnlyAffordable) {
const minCost = assetData.price * (assetData.min_purchase || 1);
if (currentCapital < minCost) return;
}
// Фильтр по доходности
if (showOnlyIncome && (!assetData.income_per_month || assetData.income_per_month <= 0)) return;
// Фильтр по моим активам
if (showMyAssets && (!assetData.player_quantity || assetData.player_quantity <= 0)) return;
filteredAssetsData[assetId] = assetData;
});
// Применяем сортировку
let sortedAssets = Object.entries(filteredAssetsData);
switch(sortBy) {
case 'price_asc':
sortedAssets.sort((a, b) => a[1].price - b[1].price);
break;
case 'price_desc':
sortedAssets.sort((a, b) => b[1].price - a[1].price);
break;
case 'volatility_asc':
sortedAssets.sort((a, b) => a[1].volatility - b[1].volatility);
break;
case 'income_desc':
sortedAssets.sort((a, b) => (b[1].income_per_month || 0) - (a[1].income_per_month || 0));
break;
default:
// По умолчанию сортируем по категории и цене
sortedAssets.sort((a, b) => {
if (a[1].category === b[1].category) {
return a[1].price - b[1].price;
}
return (a[1].category || '').localeCompare(b[1].category || '');
});
}
displayAssets(Object.fromEntries(sortedAssets));
}
// Обновляем функцию displayAssets
function displayAssets(assets) {
const assetsList = document.getElementById('assets-list');
if (!assetsList) return;
if (Object.keys(assets).length === 0) {
assetsList.innerHTML = `
<div style="text-align: center; padding: 60px 20px; color: var(--light-text);">
<div style="font-size: 4rem; margin-bottom: 20px;">🔍</div>
<h3 style="margin-bottom: 10px;">Активы не найдены</h3>
<p>Попробуйте изменить параметры фильтрации</p>
</div>
`;
return;
}
assetsList.innerHTML = '';
// Группируем по категориям
const groupedAssets = {};
Object.entries(assets).forEach(([assetId, assetData]) => {
const category = assetData.category || 'other';
if (!groupedAssets[category]) {
groupedAssets[category] = [];
}
groupedAssets[category].push({ id: assetId, ...assetData });
});
// Порядок категорий
const categoryOrder = ['bonds', 'stocks', 'real_estate', 'business', 'commodities', 'crypto', 'unique'];
categoryOrder.forEach(categoryKey => {
if (groupedAssets[categoryKey]?.length > 0) {
const category = window.assetCategories?.[categoryKey] || { name: categoryKey, icon: '📊' };
// Заголовок категории
const categoryHeader = document.createElement('div');
categoryHeader.className = 'category-header';
categoryHeader.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.5rem;">${category.icon || '📊'}</span>
<span style="font-weight: 700; color: var(--primary-color); font-size: 1.2rem;">${category.name || categoryKey}</span>
</span>
<span style="font-size: 0.9rem; color: var(--light-text);">
${groupedAssets[categoryKey].length} активов
</span>
</div>
`;
assetsList.appendChild(categoryHeader);
// Сортируем по цене
groupedAssets[categoryKey].sort((a, b) => a.price - b.price);
// Отображаем активы
groupedAssets[categoryKey].forEach(asset => {
const assetElement = createAssetElement(asset);
assetsList.appendChild(assetElement);
});
}
});
}
// Обновляем функцию createAssetElement
function createAssetElement(asset) {
const assetElement = document.createElement('div');
assetElement.className = 'asset-item';
assetElement.style.cursor = 'pointer';
assetElement.onclick = (e) => {
// Не открывать модальное окно если клик по кнопке
if (e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
showAssetDetails(asset);
}
};
const isAvailable = asset.available === null || asset.available > 0;
const minCost = asset.price * (asset.min_purchase || 1);
const canAfford = currentCapital >= minCost;
const canBuyMore = !asset.max_per_player || asset.player_quantity < asset.max_per_player;
// Прогресс-бар доступности для ограниченных активов
let availabilityHtml = '';
if (asset.available !== null && asset.total) {
const percentage = ((asset.available / asset.total) * 100).toFixed(0);
const percentageColor = percentage > 50 ? 'var(--success-color)' : percentage > 20 ? 'var(--warning-color)' : 'var(--danger-color)';
availabilityHtml = `
<div style="margin-top: 8px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 3px;">
<span style="font-size: 0.8rem; color: var(--light-text);">Доступно:</span>
<span style="font-size: 0.8rem; font-weight: 600; color: ${percentageColor};">${asset.available} / ${asset.total}</span>
</div>
<div style="height: 4px; background-color: #e0e0e0; border-radius: 2px; overflow: hidden;">
<div style="height: 100%; width: ${percentage}%; background-color: ${percentageColor}; border-radius: 2px;"></div>
</div>
</div>
`;
}
// Индикатор доходности
let incomeHtml = '';
if (asset.income_per_month > 0) {
const yearlyIncome = asset.income_per_month * 12 * 100;
incomeHtml = `
<div style="margin-top: 8px; padding: 6px; background-color: #e8f5e9; border-radius: var(--border-radius);">
<div style="display: flex; justify-content: space-between;">
<span style="font-size: 0.85rem; color: var(--success-color);">💰 Доходность:</span>
<span style="font-size: 0.85rem; font-weight: 600; color: var(--success-color);">+${(asset.income_per_month * 100).toFixed(1)}%/мес</span>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 2px;">
<span style="font-size: 0.75rem; color: var(--light-text);">В год:</span>
<span style="font-size: 0.75rem; font-weight: 600; color: var(--success-color);">+${yearlyIncome.toFixed(0)}%</span>
</div>
</div>
`;
}
assetElement.innerHTML = `
<div style="display: flex; width: 100%; gap: 20px;">
<!-- Левая колонка - информация -->
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<span style="font-size: 1.8rem;">${getAssetIcon(asset.category)}</span>
<div>
<div style="font-size: 1.2rem; font-weight: 600; color: var(--text-color);">${asset.name}</div>
<div style="display: flex; gap: 8px; margin-top: 5px;">
<span class="volatility-badge volatility-${getVolatilityLevel(asset.volatility)}">
${(asset.volatility * 100).toFixed(0)}%
</span>
<span style="font-size: 0.8rem; padding: 4px 12px; border-radius: 16px; background-color: #f0f0f0; color: var(--light-text);">
🏷️ ID: ${asset.id}
</span>
</div>
</div>
</div>
<div style="font-size: 0.9rem; color: var(--light-text); margin-bottom: 10px; line-height: 1.4;">
${asset.description || 'Нет описания'}
</div>
${availabilityHtml}
${incomeHtml}
</div>
<!-- Правая колонка - цена и действия -->
<div style="min-width: 200px; text-align: right;">
<div style="font-size: 1.6rem; font-weight: 700; color: var(--primary-color); margin-bottom: 5px;">
${formatCurrency(asset.price)}
</div>
${asset.player_quantity > 0 ?
`<div style="font-size: 0.9rem; color: var(--success-color); margin-bottom: 10px;">
📊 У вас: <strong>${asset.player_quantity} шт.</strong>
${asset.max_per_player ? `/ ${asset.max_per_player}` : ''}
</div>` : ''
}
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 15px;">
${asset.player_quantity > 0 ?
`<button onclick="event.stopPropagation(); showSellModal('${asset.id}', ${asset.price}, '${asset.name}', ${asset.player_quantity})"
class="button danger" style="width: 100%;">
📉 Продать
</button>` : ''
}
${isAvailable && canAfford && canBuyMore ?
`<button onclick="event.stopPropagation(); showBuyAssetModal('${asset.id}', ${asset.price}, '${asset.name}', ${asset.min_purchase || 1}, ${asset.available || 'null'})"
class="button success" style="width: 100%;">
📈 Купить
</button>` :
`<button disabled class="button" style="width: 100%; opacity: 0.5; cursor: not-allowed;">
${!isAvailable ? 'Нет в наличии' : !canAfford ? `Нужно ${formatCurrency(minCost)}` : 'Лимит исчерпан'}
</button>`
}
</div>
<div style="font-size: 0.8rem; color: var(--light-text); margin-top: 10px;">
Мин. лот: ${asset.min_purchase || 1} шт.
</div>
</div>
</div>
`;
return assetElement;
}
// Функция для отображения детальной информации об активе
function showAssetDetails(asset) {
// Здесь можно добавить модальное окно с детальной информацией
const message = `
${asset.name}
─────────────────
💰 Цена: ${formatCurrency(asset.price)}
📊 Волатильность: ${(asset.volatility * 100).toFixed(1)}%
${asset.income_per_month > 0 ? `💰 Доходность: +${(asset.income_per_month * 100).toFixed(1)}%/мес` : ''}
📦 Доступно: ${asset.available !== null ? asset.available : '∞'}
📈 У вас: ${asset.player_quantity} шт.
${asset.description || ''}
`;
showNotification(message, 'info', 5000);
}
// Обновляем функцию updateCapitalDisplay
function updateCapitalDisplay() {
// Обновляем капитал в шапке игры
const capitalElements = document.querySelectorAll('#modal-capital, #header-capital, #player-capital-display, .player-capital');
capitalElements.forEach(el => {
if (el.id === 'modal-capital' || el.id === 'header-capital' || el.id === 'player-capital-display') {
el.textContent = formatCurrency(currentCapital);
}
});
// Обновляем прогресс-бар
updateCapitalProgress();
// Обновляем капитал в личном профиле
const playerCapitalElement = document.querySelector('.card > div > div:last-child > div:first-child');
if (playerCapitalElement) {
playerCapitalElement.textContent = formatCurrency(currentCapital);
}
}
// Обновляем статистику активов
function updateAssetStats() {
const totalAssets = Object.values(allAssetsData).reduce((sum, asset) => {
return sum + (asset.player_quantity || 0);
}, 0);
const diversityCount = Object.values(allAssetsData).filter(asset =>
asset.player_quantity > 0
).length;
const totalElement = document.getElementById('total-assets-count');
if (totalElement) totalElement.textContent = totalAssets;
const diversityElement = document.getElementById('diversity-count');
if (diversityElement) diversityElement.textContent = diversityCount;
}
// Обновляем функцию confirmBuyAsset
function confirmBuyAsset() {
if (!selectedAsset) return;
const quantity = parseFloat(document.getElementById('buy-quantity').value) || selectedAsset.minQuantity;
const totalCost = quantity * selectedAsset.price;
if (totalCost > currentCapital) {
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(data.message, 'success');
cancelBuyAsset();
// Обновляем капитал
currentCapital = data.new_capital;
updateCapitalDisplay();
// Перезагружаем активы
loadAssets();
} else {
showNotification('❌ ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error buying asset:', error);
showNotification('❌ Ошибка при покупке', 'error');
});
}
// Обновляем функцию confirmSellAsset
function confirmSellAsset() {
if (!selectedAssetForSell) return;
const quantity = parseFloat(document.getElementById('sell-quantity').value) || 1;
if (quantity > selectedAssetForSell.maxQuantity) {
showNotification(`У вас только ${selectedAssetForSell.maxQuantity} шт.`, 'error');
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: selectedAssetForSell.id,
quantity: quantity
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, 'success');
cancelSellAsset();
// Обновляем капитал
currentCapital = data.new_capital;
updateCapitalDisplay();
// Перезагружаем активы
loadAssets();
} else {
showNotification('❌ ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error selling asset:', error);
showNotification('❌ Ошибка при продаже', 'error');
});
}
// Обновляем функцию showNotification для поддержки длительности
function showNotification(message, type = 'info', duration = 3000) {
const notification = document.createElement('div');
notification.className = `game-notification ${type}`;
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: 10001;
animation: slideInRight 0.3s ease;
max-width: 400px;
word-break: break-word;
white-space: pre-line;
font-family: monospace;
line-height: 1.5;
`;
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.opacity = '0';
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 300);
}, duration);
}
// ========== ФУНКЦИИ ДЛЯ ОБРАБОТКИ СОБЫТИЙ ==========
// Функция для отображения действий игроков
function showPlayerAction(data) {
console.log('Player action:', data);
let actionText = '';
let icon = '';
switch(data.action) {
case 'buy':
icon = '📈';
actionText = `${data.player_name} купил ${data.quantity} ${data.asset_name || data.asset_id}`;
break;
case 'sell':
icon = '📉';
actionText = `${data.player_name} продал ${data.quantity} ${data.asset_name || data.asset_id}`;
break;
case 'ability':
icon = '✨';
actionText = `${data.player_name} использовал способность: ${data.ability}`;
break;
default:
icon = '🎮';
actionText = `${data.player_name} совершил действие: ${data.action}`;
}
// Показываем уведомление о действии
showNotification(`${icon} ${actionText}`, 'info', 4000);
// Добавляем в лог событий если есть
const eventsContainer = document.getElementById('current-events');
if (eventsContainer) {
const eventElement = document.createElement('div');
eventElement.className = 'news-item';
eventElement.style.backgroundColor = '#f0f8ff';
eventElement.style.padding = '10px';
eventElement.style.marginBottom = '5px';
eventElement.style.borderRadius = 'var(--border-radius)';
eventElement.style.borderLeft = '4px solid var(--primary-color)';
eventElement.innerHTML = `
<div style="display: flex; justify-content: space-between;">
<span style="font-weight: 600;">${icon} ${data.player_name}</span>
<span style="font-size: 0.8rem; color: var(--light-text);">
${new Date(data.timestamp).toLocaleTimeString()}
</span>
</div>
<div style="margin-top: 5px;">${actionText}</div>
`;
eventsContainer.prepend(eventElement);
// Ограничиваем количество элементов
while (eventsContainer.children.length > 10) {
eventsContainer.removeChild(eventsContainer.lastChild);
}
}
}
// Функция для отображения событий игры
function showGameEvent(data) {
console.log('Game event:', data);
const eventsContainer = document.getElementById('current-events');
if (!eventsContainer) return;
let impactClass = 'impact-neutral';
let icon = '📰';
if (data.type === 'positive' || data.impact === 'positive') {
impactClass = 'impact-positive';
icon = '📈';
} else if (data.type === 'negative' || data.impact === 'negative') {
impactClass = 'impact-negative';
icon = '📉';
} else if (data.type === 'crisis') {
impactClass = 'impact-negative';
icon = '⚠️';
}
const eventElement = document.createElement('div');
eventElement.className = `news-item ${impactClass}`;
eventElement.style.padding = '12px';
eventElement.style.marginBottom = '10px';
eventElement.style.borderRadius = 'var(--border-radius)';
eventElement.style.backgroundColor = '#fff3e0';
eventElement.style.borderLeft = '4px solid var(--warning-color)';
eventElement.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-weight: 600; font-size: 1rem;">
${icon} ${data.name || 'Событие'}
</span>
<span style="font-size: 0.8rem; color: var(--light-text);">
${new Date().toLocaleDateString()}
</span>
</div>
<div style="margin-bottom: 8px; color: var(--text-color);">
${data.description || 'Произошло событие на рынке'}
</div>
`;
// Добавляем эффекты если есть
if (data.effects) {
const effectsDiv = document.createElement('div');
effectsDiv.style.marginTop = '8px';
effectsDiv.style.padding = '8px';
effectsDiv.style.backgroundColor = 'rgba(0,0,0,0.03)';
effectsDiv.style.borderRadius = 'var(--border-radius)';
effectsDiv.style.fontSize = '0.9rem';
let effectsHtml = '<div style="font-weight: 600; margin-bottom: 5px;">🎯 Эффекты:</div>';
if (typeof data.effects === 'object') {
Object.entries(data.effects).forEach(([asset, effect]) => {
if (asset === 'ALL') {
effectsHtml += `<div>• Все активы: ${effect.price_change > 0 ? '+' : ''}${(effect.price_change * 100).toFixed(0)}%</div>`;
} else {
const assetName = getAssetName(asset);
effectsHtml += `<div>• ${assetName}: ${effect.price_change > 0 ? '+' : ''}${(effect.price_change * 100).toFixed(0)}%</div>`;
}
});
}
effectsDiv.innerHTML = effectsHtml;
eventElement.appendChild(effectsDiv);
}
eventsContainer.prepend(eventElement);
// Ограничиваем количество элементов
while (eventsContainer.children.length > 15) {
eventsContainer.removeChild(eventsContainer.lastChild);
}
// Показываем уведомление
showNotification(`📢 ${data.name || 'Событие'}: ${data.description || ''}`,
data.type === 'crisis' ? 'warning' :
data.type === 'negative' ? 'error' :
data.type === 'positive' ? 'success' : 'info');
}
// Функция для отображения результатов игры
function showGameResults(data) {
console.log('Game ended:', data);
const resultsContainer = document.getElementById('month-results');
if (!resultsContainer) return;
resultsContainer.innerHTML = `
<div style="text-align: center; padding: 30px;">
<div style="font-size: 4rem; margin-bottom: 20px;">🏆</div>
<h3 style="margin-bottom: 15px; color: var(--warning-color);">ИГРА ЗАВЕРШЕНА!</h3>
<div style="margin-bottom: 20px; padding: 20px; background-color: #f8f9fa; border-radius: var(--border-radius);">
<div style="font-size: 1.2rem; margin-bottom: 10px;">Победитель:</div>
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">
${data.winner || 'Не определен'}
</div>
</div>
<button onclick="window.location.href='/rooms'" class="button success">
Вернуться к комнатам
</button>
</div>
`;
showNotification('🏆 Игра завершена!', 'success', 6000);
}
// Функция для обновления отображения рынка
function updateMarketDisplay(assets) {
const marketData = document.getElementById('market-data');
if (!marketData) return;
marketData.innerHTML = '';
const marketHeader = document.createElement('div');
marketHeader.style.marginBottom = '20px';
marketHeader.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<h4 style="margin: 0;">Текущие цены</h4>
<span style="font-size: 0.9rem; color: var(--light-text);">
Обновлено: ${new Date().toLocaleTimeString()}
</span>
</div>
`;
marketData.appendChild(marketHeader);
// Группируем по категориям
const groupedAssets = {};
Object.entries(assets).forEach(([assetId, assetData]) => {
const category = assetData.category || 'other';
if (!groupedAssets[category]) {
groupedAssets[category] = [];
}
groupedAssets[category].push({ id: assetId, ...assetData });
});
// Отображаем по категориям
Object.entries(groupedAssets).forEach(([category, categoryAssets]) => {
const categoryDiv = document.createElement('div');
categoryDiv.style.marginBottom = '20px';
categoryDiv.innerHTML = `<h5 style="margin-bottom: 10px; color: var(--primary-color);">
${getAssetIcon(category)} ${window.assetCategories?.[category]?.name || category}
</h5>`;
categoryAssets.forEach(asset => {
const assetDiv = document.createElement('div');
assetDiv.style.display = 'flex';
assetDiv.style.justifyContent = 'space-between';
assetDiv.style.padding = '8px 12px';
assetDiv.style.borderBottom = '1px solid #eee';
assetDiv.style.alignItems = 'center';
// Рассчитываем изменение цены (если есть предыдущее значение)
let priceChange = '';
let priceChangeClass = '';
if (asset.previous_price) {
const change = ((asset.price - asset.previous_price) / asset.previous_price * 100).toFixed(1);
priceChange = `${change > 0 ? '+' : ''}${change}%`;
priceChangeClass = change > 0 ? 'positive' : change < 0 ? 'negative' : 'neutral';
}
assetDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-weight: 600;">${asset.name || asset.id}</span>
<span style="font-size: 0.85rem; color: var(--light-text);">
Волат.: ${(asset.volatility * 100).toFixed(0)}%
</span>
</div>
<div style="display: flex; align-items: center; gap: 15px;">
<span style="font-weight: 700; color: var(--primary-color);">
${formatCurrency(asset.price)}
</span>
${priceChange ?
`<span class="asset-change ${priceChangeClass}" style="min-width: 60px; text-align: right;">
${priceChange}
</span>` : ''
}
</div>
`;
categoryDiv.appendChild(assetDiv);
});
marketData.appendChild(categoryDiv);
});
}
// Функция для отображения лидерборда
// Функция для получения имени актива
function getAssetName(assetId) {
const assetNames = {
'gov_bonds': 'Гособлигации',
'stock_gazprom': 'Газпром',
'stock_sberbank': 'Сбербанк',
'stock_yandex': 'Яндекс',
'apartment_small': 'Небольшая квартира',
'apartment_elite': 'Элитная квартира',
'bitcoin': 'Биткоин',
'oil': 'Нефть Brent',
'natural_gas': 'Природный газ',
'coffee_shop': 'Кофейня',
'it_startup': 'IT-стартап',
'shopping_mall': 'Торговый центр',
'oil_field': 'Нефтяное месторождение'
};
return assetNames[assetId] || assetId;
}
// Функция для форматирования валюты (дублируем для надежности)
function formatCurrency(amount) {
if (amount === undefined || amount === null) return '0 ₽';
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount).replace(',00', '');
}
function debugMonthDisplay() {
console.log('=== DEBUG MONTH DISPLAY ===');
console.log('currentMonth:', currentMonth);
console.log('totalMonths:', totalMonths);
const monthElement = document.getElementById('month-display');
console.log('monthElement:', monthElement);
if (monthElement) {
console.log('monthElement.textContent:', monthElement.textContent);
console.log('monthElement.innerHTML:', monthElement.innerHTML);
}
const timerElement = document.getElementById('phase-timer');
console.log('timerElement:', timerElement);
console.log('===========================');
}
</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;
}
}
/* Стили для категорий активов */
.category-header {
margin: 25px 0 15px 0;
padding-bottom: 10px;
border-bottom: 3px solid var(--primary-color);
position: relative;
}
.category-header:first-of-type {
margin-top: 5px;
}
.category-header span {
background-color: var(--primary-color);
color: white;
padding: 8px 16px;
border-radius: 20px;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 1rem;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.asset-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
margin-bottom: 10px;
background-color: white;
border-radius: var(--border-radius);
border: 1px solid #eee;
transition: all 0.3s ease;
}
.asset-item:hover {
transform: translateX(5px);
box-shadow: var(--box-shadow);
border-color: var(--primary-color);
}
.asset-info {
flex: 1;
padding-right: 20px;
}
.asset-name {
color: var(--text-color);
font-size: 1.1rem;
}
.asset-price {
color: var(--primary-color);
font-size: 1.3rem;
font-weight: 700;
}
.volatility-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 16px;
font-size: 0.8rem;
font-weight: 600;
}
.volatility-low {
background-color: #e8f5e9;
color: #388e3c;
}
.volatility-medium {
background-color: #fff3e0;
color: #ef6c00;
}
.volatility-high {
background-color: #ffebee;
color: #d32f2f;
}
.income-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 16px;
font-size: 0.8rem;
font-weight: 600;
background-color: #e8f5e9;
color: var(--success-color);
}
@media (max-width: 768px) {
.asset-item {
flex-direction: column;
gap: 15px;
}
.asset-info {
padding-right: 0;
width: 100%;
}
.asset-item > div:last-child {
width: 100%;
}
}
/* Стили для модального окна биржи */
#assets-modal .modal {
background-color: white;
border-radius: 20px;
padding: 25px;
max-width: 1200px;
width: 95%;
max-height: 90vh;
overflow-y: auto;
position: relative;
animation: modalFadeIn 0.3s ease;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#assets-modal .modal-backdrop {
background-color: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
}
/* Стили для скроллбара в модальном окне */
#assets-modal .modal::-webkit-scrollbar {
width: 8px;
}
#assets-modal .modal::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
#assets-modal .modal::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 4px;
}
#assets-modal .modal::-webkit-scrollbar-thumb:hover {
background: #0077b3;
}
/* Адаптивность для мобильных */
@media (max-width: 768px) {
#assets-modal .modal {
padding: 15px;
width: 100%;
height: 100%;
max-height: 100vh;
border-radius: 0;
}
#assets-modal .modal > div:first-child {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
#assets-modal .asset-item > div {
flex-direction: column;
}
#assets-modal .asset-item > div:last-child {
min-width: 100%;
}
}
.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);
min-width: 180px;
text-align: center;
}
.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; }
}
</style>
{% endblock %}