diff --git a/app.py b/app.py index 1453a42..0655363 100644 --- a/app.py +++ b/app.py @@ -579,12 +579,34 @@ def start_room_game(room_code): # Меняем статус комнаты room.status = 'playing' + + # Создаем начальное состояние игры если его нет + if not room.game_state: + game_state = GameState( + room_id=room.id, + phase='action', + phase_end=datetime.utcnow() + timedelta(seconds=120), # 2 минуты на действия + market_data=json.dumps({ + 'initialized': True, + 'assets': { + 'stock_gazprom': {'price': 1000, 'volatility': 0.2}, + 'real_estate': {'price': 3000000, 'volatility': 0.1}, + 'bitcoin': {'price': 1850000, 'volatility': 0.3}, + 'oil': {'price': 5000, 'volatility': 0.25} + }, + 'last_update': datetime.utcnow().isoformat() + }) + ) + db.session.add(game_state) + db.session.commit() # Отправляем событие через WebSocket socketio.emit('game_started', { 'room': room.code, - 'message': 'Игра началась!' + 'message': 'Игра началась!', + 'phase': 'action', + 'phase_end': room.game_state.phase_end.isoformat() if room.game_state else None }, room=room.code) return jsonify({'success': True}) @@ -896,6 +918,67 @@ def delete_room(room_code): return jsonify({'success': True}) +@app.route('/room//exit', methods=['POST']) +@login_required +def exit_lobby(room_code): + """Выход из лобби комнаты""" + room = GameRoom.query.filter_by(code=room_code).first_or_404() + + # Получаем игрока + player = GamePlayer.query.filter_by( + user_id=current_user.id, + room_id=room.id + ).first() + + if not player: + return jsonify({'error': 'Вы не находитесь в этой комнате'}), 400 + + # Если игрок - последний в комнате, удаляем комнату + player_count = GamePlayer.query.filter_by(room_id=room.id).count() + + # Если игрок - администратор и в комнате есть другие игроки + if player.is_admin and player_count > 1: + # Назначаем нового администратора + new_admin = GamePlayer.query.filter_by( + room_id=room.id, + user_id=current_user.id + ).first() + + if new_admin: + new_admin.is_admin = True + + # Удаляем игрока из комнаты + db.session.delete(player) + + # Если игрок был последним, удаляем комнату и состояние игры + if player_count == 1: + # Удаляем всех игроков (хотя остался только текущий) + GamePlayer.query.filter_by(room_id=room.id).delete() + + # Удаляем состояние игры + game_state = GameState.query.filter_by(room_id=room.id).first() + if game_state: + db.session.delete(game_state) + + # Удаляем комнату + db.session.delete(room) + + db.session.commit() + + # Отправляем WebSocket событие + socketio.emit('player_left', { + 'user_id': current_user.id, + 'username': current_user.username, + 'reason': 'exited' + }, room=room.code) + + return jsonify({ + 'success': True, + 'message': 'Вы вышли из лобби', + 'room_deleted': player_count == 1 + }) + + @app.route('/api/game//assets') @login_required def get_assets(room_code): @@ -1018,14 +1101,29 @@ def handle_join_room(data): @socketio.on('leave_room') def handle_leave_room(data): room_code = data.get('room') - if room_code: + reason = data.get('reason', 'left') # 'left', 'exited', 'kicked' + + if room_code and current_user.is_authenticated: leave_room(room_code) - logger.info(f'User {current_user.username} left room {room_code}') + logger.info(f'User {current_user.username} left room {room_code} (reason: {reason})') + + messages = { + 'left': 'покинул комнату', + 'exited': 'вышел из лобби', + 'kicked': 'был выгнан' + } emit('player_left', { 'user_id': current_user.id, - 'username': current_user.username - }, room=room_code) + 'username': current_user.username, + 'reason': reason, + 'message': f'{current_user.username} {messages.get(reason, "покинул комнату")}', + 'timestamp': datetime.utcnow().isoformat() + }, room=room_code, include_self=False) + + # Очищаем флаг уведомления в сессии + session_key = f'has_notified_join_{room_code}' + session.pop(session_key, None) @socketio.on('player_ready') @@ -1404,6 +1502,489 @@ def utility_processor(): get_ability_description=get_ability_description ) + +# Логика игры +@app.route('/game/') +@login_required +def game(room_code): + """Страница игры""" + room = GameRoom.query.filter_by(code=room_code).first_or_404() + + # Проверяем, что игрок в комнате + player = GamePlayer.query.filter_by( + user_id=current_user.id, + room_id=room.id + ).first_or_404() + + # Проверяем, что игра идет + if room.status != 'playing': + flash('Игра еще не началась или уже завершена', 'warning') + return redirect(url_for('lobby', room_code=room_code)) + + # Получаем состояние игры + game_state = room.game_state + if not game_state: + # Создаем начальное состояние игры если его нет + game_state = GameState( + room_id=room.id, + market_data=json.dumps({ + 'initialized': True, + 'assets': { + 'stock_gazprom': {'price': 1000, 'volatility': 0.2}, + 'real_estate': {'price': 3000000, 'volatility': 0.1}, + 'bitcoin': {'price': 1850000, 'volatility': 0.3}, + 'oil': {'price': 5000, 'volatility': 0.25} + }, + 'last_update': datetime.utcnow().isoformat() + }) + ) + db.session.add(game_state) + db.session.commit() + + # Получаем всех игроков для лидерборда + players = GamePlayer.query.filter_by(room_id=room.id).all() + + # Словарь названий активов для шаблона + asset_names = { + 'stock_gazprom': 'Акции Газпрома', + 'real_estate': 'Недвижимость', + 'bitcoin': 'Биткоин', + 'oil': 'Нефть' + } + + return render_template('game.html', + room=room, + player=player, + players=players, + game_state=game_state, + asset_names=asset_names) + + +# API эндпоинты для игры +@app.route('/api/game//state') +@login_required +def get_game_state(room_code): + """Получение текущего состояния игры""" + room = GameRoom.query.filter_by(code=room_code).first_or_404() + + player = GamePlayer.query.filter_by( + user_id=current_user.id, + room_id=room.id + ).first_or_404() + + game_state = room.game_state + + return jsonify({ + 'success': True, + 'phase': game_state.phase if game_state else 'action', + 'phase_end': game_state.phase_end.isoformat() if game_state and game_state.phase_end else None, + 'current_month': room.current_month, + 'total_months': room.total_months, + 'player_capital': player.capital, + 'room_status': room.status + }) + + +@app.route('/api/game//assets') +@login_required +def get_game_assets(room_code): + """Получение текущих цен активов""" + room = GameRoom.query.filter_by(code=room_code).first_or_404() + + # Проверяем, что игрок в комнате + GamePlayer.query.filter_by( + user_id=current_user.id, + room_id=room.id + ).first_or_404() + + game_state = room.game_state + if not game_state: + return jsonify({'success': False, 'error': 'Game state not found'}), 404 + + market_data = json.loads(game_state.market_data) + + return jsonify({ + 'success': True, + 'assets': market_data.get('assets', {}) + }) + + +@app.route('/api/game//buy', methods=['POST']) +@login_required +def buy_asset(room_code): + """Покупка актива""" + room = GameRoom.query.filter_by(code=room_code).first_or_404() + + player = GamePlayer.query.filter_by( + user_id=current_user.id, + room_id=room.id + ).first_or_404() + + # Проверяем, что сейчас фаза действий + if room.game_state and room.game_state.phase != 'action': + return jsonify({'success': False, 'error': 'Не фаза действий'}), 400 + + data = request.json + asset_id = data.get('asset_id') + quantity = data.get('quantity', 1) + price = data.get('price') + + if not asset_id or not price: + return jsonify({'success': False, 'error': 'Не указан актив или цена'}), 400 + + total_cost = price * quantity + + if player.capital < total_cost: + return jsonify({'success': False, 'error': 'Недостаточно средств'}), 400 + + # Получаем текущие активы игрока + assets = json.loads(player.assets) if player.assets else [] + + # Добавляем новый актив + assets.append({ + 'id': asset_id, + 'quantity': quantity, + 'purchase_price': price, + 'timestamp': datetime.utcnow().isoformat() + }) + + # Обновляем капитал и активы игрока + player.capital -= total_cost + player.assets = json.dumps(assets) + + db.session.commit() + + return jsonify({ + 'success': True, + 'new_capital': player.capital, + 'message': f'Куплено {quantity} {asset_id}' + }) + + +@app.route('/api/game//sell', methods=['POST']) +@login_required +def sell_asset(room_code): + """Продажа актива""" + room = GameRoom.query.filter_by(code=room_code).first_or_404() + + player = GamePlayer.query.filter_by( + user_id=current_user.id, + room_id=room.id + ).first_or_404() + + # Проверяем, что сейчас фаза действий + if room.game_state and room.game_state.phase != 'action': + return jsonify({'success': False, 'error': 'Не фаза действий'}), 400 + + data = request.json + asset_id = data.get('asset_id') + quantity = data.get('quantity', 1) + + if not asset_id: + return jsonify({'success': False, 'error': 'Не указан актив'}), 400 + + # Получаем текущие активы игрока + assets = json.loads(player.assets) if player.assets else [] + + # Ищем актив для продажи + asset_to_sell = None + for asset in assets: + if asset['id'] == asset_id and asset['quantity'] >= quantity: + asset_to_sell = asset + break + + if not asset_to_sell: + return jsonify({'success': False, 'error': 'Недостаточно активов для продажи'}), 400 + + # Получаем текущую цену актива + game_state = room.game_state + if not game_state: + return jsonify({'success': False, 'error': 'Состояние игры не найдено'}), 404 + + market_data = json.loads(game_state.market_data) + current_price = market_data.get('assets', {}).get(asset_id, {}).get('price', asset_to_sell['purchase_price']) + + # Рассчитываем выручку + revenue = current_price * quantity + + # Обновляем активы игрока + asset_to_sell['quantity'] -= quantity + if asset_to_sell['quantity'] <= 0: + assets.remove(asset_to_sell) + + # Обновляем капитал + player.capital += revenue + player.assets = json.dumps(assets) + + db.session.commit() + + return jsonify({ + 'success': True, + 'new_capital': player.capital, + 'revenue': revenue, + 'message': f'Продано {quantity} {asset_id}' + }) + + +@app.route('/api/game//use_ability', methods=['POST']) +@login_required +def use_ability(room_code): + """Использование способности""" + room = GameRoom.query.filter_by(code=room_code).first_or_404() + + player = GamePlayer.query.filter_by( + user_id=current_user.id, + room_id=room.id + ).first_or_404() + + if not player.ability: + return jsonify({'success': False, 'error': 'У вас нет способности'}), 400 + + # Здесь должна быть логика применения способности + # Пока просто возвращаем успех + + return jsonify({ + 'success': True, + 'ability_name': get_ability_name(player.ability), + 'message': 'Способность использована' + }) + + +@app.route('/api/game//end_action_phase', methods=['POST']) +@login_required +def end_action_phase(room_code): + """Завершение фазы действий""" + room = GameRoom.query.filter_by(code=room_code).first_or_404() + + player = GamePlayer.query.filter_by( + user_id=current_user.id, + room_id=room.id, + is_admin=True + ).first() + + if not player: + return jsonify({'success': False, 'error': 'Только администратор может завершать фазы'}), 403 + + if not room.game_state or room.game_state.phase != 'action': + return jsonify({'success': False, 'error': 'Сейчас не фаза действий'}), 400 + + # Переходим к следующей фазе + game_state = room.game_state + game_state.phase = 'market' + game_state.phase_end = datetime.utcnow() + timedelta(seconds=30) # 30 секунд на реакцию рынка + + db.session.commit() + + # Отправляем WebSocket событие + socketio.emit('game_phase_changed', { + 'room': room.code, + 'phase': 'market', + 'phase_end': game_state.phase_end.isoformat(), + 'message': 'Фаза действий завершена, начинается реакция рынка' + }, room=room.code) + + # Запускаем расчет рынка + socketio.start_background_task(calculate_and_update_market, room.id) + + return jsonify({'success': True}) + + +def calculate_and_update_market(room_id): + """Расчет реакции рынка (в фоновом потоке)""" + import time + + # Ждем 5 секунд перед расчетом + time.sleep(5) + + room = GameRoom.query.get(room_id) + if not room or room.status != 'playing': + return + + game_state = room.game_state + if not game_state or game_state.phase != 'market': + return + + # Получаем всех игроков + players = GamePlayer.query.filter_by(room_id=room_id).all() + + # Получаем текущие данные рынка + market_data = json.loads(game_state.market_data) if game_state.market_data else {} + assets = market_data.get('assets', {}) + + # Базовые цены для расчета перегрева + base_prices = { + 'stock_gazprom': 1000, + 'real_estate': 3000000, + 'bitcoin': 1850000, + 'oil': 5000 + } + + # Считаем спрос по каждому активу + for asset_id in assets.keys(): + demand = 0 + for player in players: + player_assets = json.loads(player.assets) if player.assets else [] + for player_asset in player_assets: + if player_asset.get('id') == asset_id: + demand += player_asset.get('quantity', 0) + + # Формула влияния спроса + total_players = len(players) + if total_players > 0 and asset_id in assets: + volatility = assets[asset_id].get('volatility', 0.2) + price_change = volatility * (demand / (total_players * 10)) + new_price = assets[asset_id]['price'] * (1 + price_change) + + # Эффект перегрева + if base_prices.get(asset_id) and new_price > base_prices[asset_id] * (1 + 2 * volatility): + new_price *= random.uniform(0.7, 0.9) + + assets[asset_id]['price'] = new_price + + # Обновляем данные рынка + market_data['assets'] = assets + market_data['last_update'] = datetime.utcnow().isoformat() + + game_state.market_data = json.dumps(market_data) + db.session.commit() + + # Отправляем обновление через WebSocket + socketio.emit('market_updated', { + 'room': room.code, + 'assets': assets, + 'timestamp': datetime.utcnow().isoformat() + }, room=room.code) + + # Через 25 секунд переходим к событиям + time.sleep(25) + + # Проверяем, что мы все еще в фазе рынка + room = GameRoom.query.get(room_id) + if room and room.game_state and room.game_state.phase == 'market': + game_state = room.game_state + game_state.phase = 'event' + game_state.phase_end = datetime.utcnow() + timedelta(seconds=30) + + db.session.commit() + + socketio.emit('game_phase_changed', { + 'room': room.code, + 'phase': 'event', + 'phase_end': game_state.phase_end.isoformat(), + 'message': 'Реакция рынка завершена, начинаются случайные события' + }, room=room.code) + + +@app.route('/api/game//leaderboard') +@login_required +def get_game_leaderboard(room_code): + """Получение таблицы лидеров""" + room = GameRoom.query.filter_by(code=room_code).first_or_404() + + # Проверяем, что игрок в комнате + GamePlayer.query.filter_by( + user_id=current_user.id, + room_id=room.id + ).first_or_404() + + # Получаем всех игроков + players = GamePlayer.query.filter_by(room_id=room.id).all() + + players_data = [] + for player in players: + user = User.query.get(player.user_id) + players_data.append({ + 'user_id': player.user_id, + 'username': user.username if user else 'Игрок', + 'capital': player.capital, + 'ability': player.ability, + 'is_admin': player.is_admin + }) + + return jsonify({ + 'success': True, + 'players': players_data + }) + + +# WebSocket обработчики для игры +@socketio.on('join_game_room') +def handle_join_game_room(data): + """Присоединение к комнате игры""" + room_code = data.get('room') + if room_code and current_user.is_authenticated: + join_room(room_code) + logger.info(f'User {current_user.username} joined game room {room_code}') + + emit('player_joined_game', { + 'user_id': current_user.id, + 'username': current_user.username, + 'timestamp': datetime.utcnow().isoformat() + }, room=room_code, include_self=False) + + +@socketio.on('leave_game_room') +def handle_leave_game_room(data): + """Выход из игровой комнаты""" + room_code = data.get('room') + reason = data.get('reason', 'left') + + if room_code and current_user.is_authenticated: + leave_room(room_code) + logger.info(f'User {current_user.username} left game room {room_code} (reason: {reason})') + + emit('player_left_game', { + 'user_id': current_user.id, + 'username': current_user.username, + 'reason': reason, + 'timestamp': datetime.utcnow().isoformat() + }, room=room_code, include_self=False) + + +@socketio.on('game_chat_message') +def handle_game_chat_message(data): + """Обработка сообщений в игровом чате""" + room_code = data.get('room') + message = data.get('message', '').strip() + + if message and room_code and current_user.is_authenticated: + emit('game_chat_message', { + 'user_id': current_user.id, + 'username': current_user.username, + 'message': message, + 'timestamp': datetime.utcnow().isoformat() + }, room=room_code) + + +@socketio.on('player_bought_asset') +def handle_player_bought_asset(data): + """Игрок купил актив""" + room_code = data.get('room') + if room_code: + emit('player_action', { + 'player_id': current_user.id, + 'player_name': current_user.username, + 'action': 'buy', + 'asset_id': data.get('asset_id'), + 'quantity': data.get('quantity'), + 'price': data.get('price'), + 'timestamp': datetime.utcnow().isoformat() + }, room=room_code, include_self=False) + + +@socketio.on('player_used_ability') +def handle_player_used_ability(data): + """Игрок использовал способность""" + room_code = data.get('room') + if room_code: + emit('player_action', { + 'player_id': current_user.id, + 'player_name': current_user.username, + 'action': 'ability', + 'ability': data.get('ability'), + 'timestamp': datetime.utcnow().isoformat() + }, room=room_code, include_self=False) + # --- ТОЧКА ВХОДА --- if __name__ == '__main__': diff --git a/templates/base.html b/templates/base.html index b6ef7b0..955af71 100644 --- a/templates/base.html +++ b/templates/base.html @@ -62,6 +62,22 @@ socket.on('player_left', function(data) { console.log('Player left:', data.username); }); + + socket.on('player_joined_game', function(data) { + console.log('Player joined game:', data.username); + }); + + socket.on('player_left_game', function(data) { + console.log('Player left game:', data.username); + }); + + socket.on('player_action', function(data) { + console.log('Player action in game:', data); + }); + + socket.on('game_chat_message', function(data) { + console.log('Game chat message:', data); + }); {% block scripts %}{% endblock %} diff --git a/templates/game.html b/templates/game.html index fe157c4..3f5fb65 100644 --- a/templates/game.html +++ b/templates/game.html @@ -1,40 +1,1275 @@ - - - - - - Капитал & Рынок - Игра - - - -
-
- -
- -
- Месяц 1 -
+{% extends "base.html" %} -
- - - +{% block title %}Игра: {{ room.name }} - Капитал & Рынок{% endblock %} + +{% block content %} +
+ +
+ + ← + +
+ + 🎮 {{ room.name }} + +
+
+
+ {{ game_state.phase|capitalize }}: 2:00 +
+
+ Месяц {{ room.current_month }}/{{ room.total_months }} +
- - - - - \ No newline at end of file +} + +function displayAssets(assets) { + const assetsList = document.getElementById('assets-list'); + if (!assetsList) return; + + assetsList.innerHTML = ''; + + Object.entries(assets).forEach(([assetId, assetData]) => { + const assetElement = document.createElement('div'); + assetElement.className = 'asset-item'; + assetElement.innerHTML = ` +
+
+ ${getAssetName(assetId)} +
+
+ Волатильность: ${(assetData.volatility * 100).toFixed(1)}% +
+
+
+
+ ${formatCurrency(assetData.price)} +
+ +
+ `; + + assetsList.appendChild(assetElement); + }); +} + +function getAssetName(assetId) { + const names = { + 'stock_gazprom': 'Акции Газпрома', + 'real_estate': 'Недвижимость', + 'bitcoin': 'Биткоин', + 'oil': 'Нефть' + }; + return names[assetId] || assetId; +} + +function showBuyAssetModal(assetId, price) { + selectedAsset = { id: assetId, price: price }; + + document.getElementById('buy-asset-title').textContent = `Покупка ${getAssetName(assetId)}`; + document.getElementById('asset-price').textContent = formatCurrency(price); + document.getElementById('player-capital-display').textContent = formatCurrency(playerCapital); + + const maxQuantity = Math.floor(playerCapital / price); + document.getElementById('max-quantity').textContent = `${maxQuantity} шт.`; + + document.getElementById('buy-quantity').max = maxQuantity; + document.getElementById('buy-quantity').value = 1; + + updateBuyCalculations(); + + document.getElementById('buy-asset-modal').classList.add('active'); +} + +function updateBuyCalculations() { + if (!selectedAsset) return; + + const quantity = parseInt(document.getElementById('buy-quantity').value) || 1; + const price = selectedAsset.price; + const totalCost = quantity * price; + + document.getElementById('total-cost').textContent = formatCurrency(totalCost); + document.getElementById('remaining-capital').textContent = formatCurrency(playerCapital - totalCost); +} + +function confirmBuyAsset() { + if (!selectedAsset) return; + + const quantity = parseInt(document.getElementById('buy-quantity').value) || 1; + const totalCost = quantity * selectedAsset.price; + + if (totalCost > playerCapital) { + showNotification('❌ Недостаточно средств', 'error'); + return; + } + + fetch(`/api/game/${roomCode}/buy`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}' + }, + body: JSON.stringify({ + asset_id: selectedAsset.id, + quantity: quantity, + price: selectedAsset.price + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showNotification(`✅ Куплено ${quantity} ${getAssetName(selectedAsset.id)}`, 'success'); + cancelBuyAsset(); + + // Обновляем капитал + playerCapital = data.new_capital; + updateCapitalProgress(); + loadAssets(); // Перезагружаем активы + + // Отправляем WebSocket событие + if (typeof socket !== 'undefined') { + socket.emit('player_bought_asset', { + room: roomCode, + player_id: playerId, + asset_id: selectedAsset.id, + quantity: quantity, + price: selectedAsset.price + }); + } + } else { + showNotification('❌ Ошибка: ' + data.error, 'error'); + } + }) + .catch(error => { + console.error('Error buying asset:', error); + showNotification('❌ Ошибка при покупке', 'error'); + }); +} + +function cancelBuyAsset() { + selectedAsset = null; + document.getElementById('buy-asset-modal').classList.remove('active'); +} + +// Продажа активов +function sellAsset(assetId, quantity) { + if (!confirm(`Продать ${quantity} ${getAssetName(assetId)}?`)) { + return; + } + + fetch(`/api/game/${roomCode}/sell`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}' + }, + body: JSON.stringify({ + asset_id: assetId, + quantity: quantity + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showNotification(`✅ Продано ${quantity} ${getAssetName(assetId)}`, 'success'); + updateGameState(); + } else { + showNotification('❌ Ошибка: ' + data.error, 'error'); + } + }) + .catch(error => { + console.error('Error selling asset:', error); + showNotification('❌ Ошибка при продаже', 'error'); + }); +} + +// Использование способности +function useAbility() { + fetch(`/api/game/${roomCode}/use_ability`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showNotification(`✨ Использована способность: ${data.ability_name}`, 'success'); + + if (typeof socket !== 'undefined') { + socket.emit('player_used_ability', { + room: roomCode, + player_id: playerId, + ability: data.ability_name + }); + } + } else { + showNotification('❌ Ошибка: ' + data.error, 'error'); + } + }) + .catch(error => { + console.error('Error using ability:', error); + showNotification('❌ Ошибка при использовании способности', 'error'); + }); +} + +// Завершение фазы действий +function endActionPhase() { + if (!confirm('Завершить ваши действия и перейти к следующей фазе?')) { + return; + } + + fetch(`/api/game/${roomCode}/end_action_phase`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showNotification('✅ Фаза действий завершена', 'success'); + } else { + showNotification('❌ Ошибка: ' + data.error, 'error'); + } + }) + .catch(error => { + console.error('Error ending action phase:', error); + showNotification('❌ Ошибка при завершении фазы', 'error'); + }); +} + +function skipTurn() { + if (confirm('Пропустить ход? Вы не сможете совершать действия в этой фазе.')) { + endActionPhase(); + } +} + +// Обновление лидерборда +function updateLeaderboard() { + fetch(`/api/game/${roomCode}/leaderboard`) + .then(response => response.json()) + .then(data => { + if (data.success && data.players) { + displayLeaderboard(data.players); + } + }) + .catch(error => { + console.error('Error loading leaderboard:', error); + showNotification('❌ Ошибка загрузки лидерборда', 'error'); + }); +} + +function displayLeaderboard(players) { + const leaderboardElement = document.getElementById('leaderboard'); + if (!leaderboardElement) return; + + leaderboardElement.innerHTML = ''; + + players.sort((a, b) => b.capital - a.capital); + + players.forEach((player, index) => { + const position = index + 1; + const isCurrentPlayer = player.user_id === {{ current_user.id }}; + + const playerElement = document.createElement('div'); + playerElement.className = 'player-item'; + playerElement.style.backgroundColor = isCurrentPlayer ? '#f0f8ff' : 'transparent'; + playerElement.style.borderLeft = isCurrentPlayer ? '4px solid var(--primary-color)' : 'none'; + + playerElement.innerHTML = ` +
+
+ ${position} +
+
+
+ ${player.username} + ${isCurrentPlayer ? + 'Вы' : + ''} +
+
+ ${getAbilityName(player.ability)} +
+
+
+
+ ${formatCurrency(player.capital)} +
+ `; + + 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 = '
Нет сообщений
'; +} + +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 = ` +
+ ${data.username} + ${time} +
+
${data.message}
+ `; + + chatContainer.appendChild(messageElement); + chatContainer.scrollTop = chatContainer.scrollHeight; +} + +// Функции выхода из игры +function confirmExitGame() { + showExitModal(); + return false; +} + +function confirmExitGameAction() { + // Закрываем модальное окно + document.getElementById('exit-game-modal').classList.remove('active'); + + // Выполняем выход + performExitGame(); +} + +function cancelExitGame() { + document.getElementById('exit-game-modal').classList.remove('active'); +} + +function showExitModal(event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + document.getElementById('exit-game-modal').classList.add('active'); +} + +function performExitGame() { + // Отправляем WebSocket событие о выходе если подключены + if (typeof socket !== 'undefined' && socket.connected && roomCode) { + socket.emit('leave_game_room', { + room: roomCode, + reason: 'exited' + }); + } + + // Редирект в лобби + showNotification('🔄 Перенаправление в лобби...', 'info'); + setTimeout(() => { + window.location.href = '/room/' + roomCode; + }, 1000); +} + +// Вспомогательные функции +function formatCurrency(amount) { + return new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB', + minimumFractionDigits: 0 + }).format(amount); +} + +// Обновляем поле количества при изменении +document.getElementById('buy-quantity')?.addEventListener('input', updateBuyCalculations); + +// Закрытие модальных окон +document.addEventListener('click', function(event) { + if (event.target.classList.contains('modal-backdrop')) { + document.getElementById('exit-game-modal').classList.remove('active'); + document.getElementById('buy-asset-modal').classList.remove('active'); + } +}); + +// Обработка клавиши Escape +document.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + document.getElementById('exit-game-modal').classList.remove('active'); + document.getElementById('buy-asset-modal').classList.remove('active'); + } +}); + + + +{% endblock %} \ No newline at end of file diff --git a/templates/lobby.html b/templates/lobby.html index d790b91..904f1db 100644 --- a/templates/lobby.html +++ b/templates/lobby.html @@ -30,20 +30,22 @@

{{ room.name }}

Код комнаты: {{ room.code }} -
{% if room.status == 'waiting' %} - Ожидание + Ожидание {% elif room.status == 'playing' %} - Игра идет + Игра идет {% elif room.status == 'finished' %} - Завершена + Завершена {% else %} - {{ room.status }} + {{ room.status }} {% endif %}
@@ -79,63 +81,64 @@ {% if room.settings %} - {% set settings_dict = room.settings|from_json %} -
-

Настройки игры:

-
- {% if settings_dict.allow_loans %} - + {% set settings_dict = room.settings|from_json %} +
+

Настройки игры:

+
+ {% if settings_dict.allow_loans %} + ✅ Кредиты разрешены - {% else %} - + {% else %} + ❌ Кредиты запрещены - {% endif %} + {% endif %} - {% if settings_dict.allow_black_market %} - + {% if settings_dict.allow_black_market %} + ⚫ Чёрный рынок - {% else %} - + {% else %} + ⚪ Без чёрного рынка - {% endif %} + {% endif %} - {% if settings_dict.private_room %} - + {% if settings_dict.private_room %} + 🔒 Приватная - {% else %} - + {% else %} + 🔓 Публичная - {% endif %} -
+ {% endif %}
+
{% endif %}
{% if current_player.is_admin and room.status == 'waiting' %} - + {% elif room.status == 'playing' %} - + {% endif %} {% if room.status == 'waiting' %} - + {% endif %} {% if current_player.is_admin %} - + {% endif %}
+
+ +
+ {% if room.status == 'waiting' %} -
-
- Готовы к старту: - {{ players|selectattr('is_ready')|list|length }}/{{ players|length }} -
-
-
-
-
- {% if players|length < 2 %} -
- ⚠️ Нужно минимум 2 игрока для начала игры -
- {% endif %} +
+
+ Готовы к старту: + {{ players|selectattr('is_ready')|list|length }}/{{ players|length }}
+
+
+
+
+ {% if players|length < 2 %} +
+ ⚠️ Нужно минимум 2 игрока для начала игры +
+ {% endif %} +
{% endif %}
@@ -179,58 +188,60 @@

👥 Участники ({{ players|length }})

{% if players %} -
    - {% for player in players %} -
  • -
    -
    - {{ player.user.username[0]|upper if player.user and player.user.username else '?' }} -
    -
    -
    - {{ player.user.username if player.user else 'Игрок' }} - {% if player.is_admin %} - Админ - {% endif %} - {% if player.user_id == current_user.id %} - Вы - {% endif %} -
    -
    - {{ player.capital|format_currency }} -
    -
    -
    -
    - {% if room.status == 'waiting' %} -
    - {% if player.is_ready %} - ✅ Готов - {% else %} - ⏳ Ожидание - {% endif %} -
    +
      + {% for player in players %} +
    • +
      +
      + {{ player.user.username[0]|upper if player.user and player.user.username else '?' }} +
      +
      +
      + {{ player.user.username if player.user else 'Игрок' }} + {% if player.is_admin %} + Админ + {% endif %} + {% if player.user_id == current_user.id %} + Вы {% endif %} -
      - {{ get_ability_name(player.ability) }} -
      -
    • - {% endfor %} -
    +
    + {{ player.capital|format_currency }} +
    +
    +
+
+ {% if room.status == 'waiting' %} +
+ {% if player.is_ready %} + ✅ Готов + {% else %} + ⏳ Ожидание + {% endif %} +
+ {% endif %} +
+ {{ get_ability_name(player.ability) }} +
+
+ + {% endfor %} + {% else %} -
-
👤
-

В комнате пока нет игроков

-

Пригласите друзей!

-
+
+
👤
+

В комнате пока нет игроков

+

Пригласите друзей!

+
{% endif %}

💬 Чат комнаты

-
+
Чат загружается...
@@ -250,11 +261,14 @@

📊 Статистика комнаты

-
{{ players|length }}
+
{{ players|length }} +
Игроков
-
{{ room.current_month }}
+
{{ room.current_month + }} +
Текущий месяц
@@ -266,9 +280,9 @@
{% if players|length > 0 %} - {{ ((players|selectattr('is_ready')|list|length / players|length * 100)|round(1)) }}% + {{ ((players|selectattr('is_ready')|list|length / players|length * 100)|round(1)) }}% {% else %} - 0% + 0% {% endif %}
Готовы
@@ -293,10 +307,10 @@
@@ -309,11 +323,11 @@
@@ -322,11 +336,11 @@
@@ -364,11 +378,13 @@
-
Отсканируйте QR-код
+
Отсканируйте QR-код +
📱
-
{{ room.code }}
+
{{ room.code }} +
@@ -386,16 +402,18 @@

Управление игроками:

-
+
{% for player in players %} - {% if not player.is_admin %} -
- {{ player.user.username if player.user else 'Игрок' }} - -
- {% endif %} + {% if not player.is_admin %} +
+ {{ player.user.username if player.user else 'Игрок' }} + +
+ {% endif %} {% endfor %}
@@ -422,815 +440,927 @@ {% block scripts %} {% endblock %} \ No newline at end of file