2966 lines
118 KiB
HTML
2966 lines
118 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">
|
||
Фаза действий
|
||
</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 %} |