Game page
This commit is contained in:
591
app.py
591
app.py
@@ -579,12 +579,34 @@ def start_room_game(room_code):
|
|||||||
|
|
||||||
# Меняем статус комнаты
|
# Меняем статус комнаты
|
||||||
room.status = 'playing'
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
# Отправляем событие через WebSocket
|
# Отправляем событие через WebSocket
|
||||||
socketio.emit('game_started', {
|
socketio.emit('game_started', {
|
||||||
'room': room.code,
|
'room': room.code,
|
||||||
'message': 'Игра началась!'
|
'message': 'Игра началась!',
|
||||||
|
'phase': 'action',
|
||||||
|
'phase_end': room.game_state.phase_end.isoformat() if room.game_state else None
|
||||||
}, room=room.code)
|
}, room=room.code)
|
||||||
|
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
@@ -896,6 +918,67 @@ def delete_room(room_code):
|
|||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/room/<room_code>/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/<room_code>/assets')
|
@app.route('/api/game/<room_code>/assets')
|
||||||
@login_required
|
@login_required
|
||||||
def get_assets(room_code):
|
def get_assets(room_code):
|
||||||
@@ -1018,14 +1101,29 @@ def handle_join_room(data):
|
|||||||
@socketio.on('leave_room')
|
@socketio.on('leave_room')
|
||||||
def handle_leave_room(data):
|
def handle_leave_room(data):
|
||||||
room_code = data.get('room')
|
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)
|
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', {
|
emit('player_left', {
|
||||||
'user_id': current_user.id,
|
'user_id': current_user.id,
|
||||||
'username': current_user.username
|
'username': current_user.username,
|
||||||
}, room=room_code)
|
'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')
|
@socketio.on('player_ready')
|
||||||
@@ -1404,6 +1502,489 @@ def utility_processor():
|
|||||||
get_ability_description=get_ability_description
|
get_ability_description=get_ability_description
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Логика игры
|
||||||
|
@app.route('/game/<room_code>')
|
||||||
|
@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/<room_code>/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/<room_code>/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/<room_code>/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/<room_code>/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/<room_code>/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/<room_code>/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/<room_code>/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__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -62,6 +62,22 @@
|
|||||||
socket.on('player_left', function(data) {
|
socket.on('player_left', function(data) {
|
||||||
console.log('Player left:', data.username);
|
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);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|||||||
1291
templates/game.html
1291
templates/game.html
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,9 @@
|
|||||||
<h3 style="margin-bottom: 5px;">{{ room.name }}</h3>
|
<h3 style="margin-bottom: 5px;">{{ room.name }}</h3>
|
||||||
<div style="color: var(--light-text); font-size: 0.9rem;">
|
<div style="color: var(--light-text); font-size: 0.9rem;">
|
||||||
Код комнаты: <strong>{{ room.code }}</strong>
|
Код комнаты: <strong>{{ room.code }}</strong>
|
||||||
<button onclick="copyRoomCode()" style="background: none; border: none; color: var(--primary-color); cursor: pointer; margin-left: 5px;" title="Скопировать код">
|
<button onclick="copyRoomCode()"
|
||||||
|
style="background: none; border: none; color: var(--primary-color); cursor: pointer; margin-left: 5px;"
|
||||||
|
title="Скопировать код">
|
||||||
📋
|
📋
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +131,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if room.status == 'waiting' %}
|
{% if room.status == 'waiting' %}
|
||||||
<button id="ready-btn" onclick="toggleReady()" class="button {% if current_player.is_ready %}success{% else %}secondary{% endif %}">
|
<button id="ready-btn" onclick="toggleReady()"
|
||||||
|
class="button {% if current_player.is_ready %}success{% else %}secondary{% endif %}">
|
||||||
{% if current_player.is_ready %}
|
{% if current_player.is_ready %}
|
||||||
✅ Готов
|
✅ Готов
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -153,6 +156,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<button onclick="exitLobby()" class="button danger" style="width: 100%;">
|
||||||
|
🚪 Выйти из лобби
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Индикатор готовности -->
|
<!-- Индикатор готовности -->
|
||||||
{% if room.status == 'waiting' %}
|
{% if room.status == 'waiting' %}
|
||||||
<div style="margin-top: 15px; padding: 10px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
<div style="margin-top: 15px; padding: 10px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
||||||
@@ -183,7 +192,8 @@
|
|||||||
{% for player in players %}
|
{% for player in players %}
|
||||||
<li class="player-item">
|
<li class="player-item">
|
||||||
<div class="player-info">
|
<div class="player-info">
|
||||||
<div class="player-avatar" style="background-color: {% if player.is_admin %}#ff9800{% else %}var(--primary-color){% endif %};">
|
<div class="player-avatar"
|
||||||
|
style="background-color: {% if player.is_admin %}#ff9800{% else %}var(--primary-color){% endif %};">
|
||||||
{{ player.user.username[0]|upper if player.user and player.user.username else '?' }}
|
{{ player.user.username[0]|upper if player.user and player.user.username else '?' }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -230,7 +240,8 @@
|
|||||||
<!-- Чат комнаты -->
|
<!-- Чат комнаты -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>💬 Чат комнаты</h3>
|
<h3>💬 Чат комнаты</h3>
|
||||||
<div id="chat-messages" style="height: 200px; overflow-y: auto; margin-bottom: 10px; padding: 10px; background-color: #f8f9fa; border-radius: var(--border-radius);">
|
<div id="chat-messages"
|
||||||
|
style="height: 200px; 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 style="text-align: center; color: var(--light-text); padding: 20px;">
|
||||||
Чат загружается...
|
Чат загружается...
|
||||||
</div>
|
</div>
|
||||||
@@ -250,11 +261,14 @@
|
|||||||
<h3>📊 Статистика комнаты</h3>
|
<h3>📊 Статистика комнаты</h3>
|
||||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-top: 15px;">
|
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-top: 15px;">
|
||||||
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
||||||
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">{{ players|length }}</div>
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">{{ players|length }}
|
||||||
|
</div>
|
||||||
<div style="font-size: 0.9rem; color: var(--light-text);">Игроков</div>
|
<div style="font-size: 0.9rem; color: var(--light-text);">Игроков</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
||||||
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">{{ room.current_month }}</div>
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">{{ room.current_month
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
<div style="font-size: 0.9rem; color: var(--light-text);">Текущий месяц</div>
|
<div style="font-size: 0.9rem; color: var(--light-text);">Текущий месяц</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
||||||
@@ -293,10 +307,10 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="edit-total-months">Длительность игры</label>
|
<label for="edit-total-months">Длительность игры</label>
|
||||||
<select id="edit-total-months" name="total_months">
|
<select id="edit-total-months" name="total_months">
|
||||||
<option value="6" {% if room.total_months == 6 %}selected{% endif %}>6 месяцев</option>
|
<option value="6" {% if room.total_months== 6 %}selected{% endif %}>6 месяцев</option>
|
||||||
<option value="12" {% if room.total_months == 12 %}selected{% endif %}>12 месяцев</option>
|
<option value="12" {% if room.total_months== 12 %}selected{% endif %}>12 месяцев</option>
|
||||||
<option value="18" {% if room.total_months == 18 %}selected{% endif %}>18 месяцев</option>
|
<option value="18" {% if room.total_months== 18 %}selected{% endif %}>18 месяцев</option>
|
||||||
<option value="24" {% if room.total_months == 24 %}selected{% endif %}>24 месяца</option>
|
<option value="24" {% if room.total_months== 24 %}selected{% endif %}>24 месяца</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -364,11 +378,13 @@
|
|||||||
<div id="qrcode" style="text-align: center; margin: 15px 0;">
|
<div id="qrcode" style="text-align: center; margin: 15px 0;">
|
||||||
<!-- QR-код будет сгенерирован JavaScript -->
|
<!-- QR-код будет сгенерирован JavaScript -->
|
||||||
<div style="background-color: white; padding: 10px; display: inline-block; border-radius: var(--border-radius);">
|
<div style="background-color: white; padding: 10px; display: inline-block; border-radius: var(--border-radius);">
|
||||||
<div style="font-size: 0.8rem; color: var(--light-text); margin-bottom: 5px;">Отсканируйте QR-код</div>
|
<div style="font-size: 0.8rem; color: var(--light-text); margin-bottom: 5px;">Отсканируйте QR-код
|
||||||
|
</div>
|
||||||
<div style="width: 150px; height: 150px; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; border-radius: 5px;">
|
<div style="width: 150px; height: 150px; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; border-radius: 5px;">
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<div style="font-size: 3rem;">📱</div>
|
<div style="font-size: 3rem;">📱</div>
|
||||||
<div style="font-size: 0.7rem; color: var(--light-text); margin-top: 5px;">{{ room.code }}</div>
|
<div style="font-size: 0.7rem; color: var(--light-text); margin-top: 5px;">{{ room.code }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,12 +402,14 @@
|
|||||||
|
|
||||||
<div style="margin: 15px 0;">
|
<div style="margin: 15px 0;">
|
||||||
<h4>Управление игроками:</h4>
|
<h4>Управление игроками:</h4>
|
||||||
<div id="admin-player-list" style="max-height: 200px; overflow-y: auto; margin: 10px 0; padding: 10px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
<div id="admin-player-list"
|
||||||
|
style="max-height: 200px; overflow-y: auto; margin: 10px 0; padding: 10px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
||||||
{% for player in players %}
|
{% for player in players %}
|
||||||
{% if not player.is_admin %}
|
{% if not player.is_admin %}
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #ddd;">
|
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #ddd;">
|
||||||
<span>{{ player.user.username if player.user else 'Игрок' }}</span>
|
<span>{{ player.user.username if player.user else 'Игрок' }}</span>
|
||||||
<button onclick="kickPlayer({{ player.user_id }})" class="button danger" style="padding: 5px 10px; font-size: 0.8rem;">
|
<button onclick="kickPlayer({{ player.user_id }})" class="button danger"
|
||||||
|
style="padding: 5px 10px; font-size: 0.8rem;">
|
||||||
Выгнать
|
Выгнать
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -422,24 +440,25 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
// Глобальные переменные
|
// Глобальные переменные
|
||||||
let roomCode = '{{ room.code }}';
|
let roomCode = '{{ room.code }}';
|
||||||
let currentPlayerId = '{{ current_player.id }}';
|
let currentPlayerId = '{{ current_player.id }}';
|
||||||
let isAdmin = {{ 'true' if current_player.is_admin else 'false' }};
|
let isAdmin = {{ 'true' if current_player.is_admin else 'false' }};
|
||||||
let lastNotificationTime = 0; // Для ограничения частоты уведомлений
|
let lastNotificationTime = 0; // Для ограничения частоты уведомлений
|
||||||
window.roomStatus = '{{ room.status }}'; // Добавьте эту строку
|
window.roomStatus = '{{ room.status }}'; // Добавьте эту строку
|
||||||
|
window.userExitedLobby = false;
|
||||||
|
|
||||||
// Функция форматирования валюты
|
// Функция форматирования валюты
|
||||||
function formatCurrency(amount) {
|
function formatCurrency(amount) {
|
||||||
return new Intl.NumberFormat('ru-RU', {
|
return new Intl.NumberFormat('ru-RU', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
minimumFractionDigits: 0
|
minimumFractionDigits: 0
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация при загрузке
|
// Инициализация при загрузке
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
console.log('Lobby page loaded for room:', roomCode);
|
console.log('Lobby page loaded for room:', roomCode);
|
||||||
|
|
||||||
// Проверяем, что socket существует
|
// Проверяем, что socket существует
|
||||||
@@ -455,45 +474,45 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Периодическое обновление статуса
|
// Периодическое обновление статуса
|
||||||
setInterval(updateRoomStatus, 5000);
|
setInterval(updateRoomStatus, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Функции управления модальными окнами
|
// Функции управления модальными окнами
|
||||||
function showSettings() {
|
function showSettings() {
|
||||||
console.log('Opening settings modal');
|
console.log('Opening settings modal');
|
||||||
document.getElementById('settings-modal').classList.add('active');
|
document.getElementById('settings-modal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideSettings() {
|
function hideSettings() {
|
||||||
console.log('Closing settings modal');
|
console.log('Closing settings modal');
|
||||||
document.getElementById('settings-modal').classList.remove('active');
|
document.getElementById('settings-modal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyInviteLink() {
|
function copyInviteLink() {
|
||||||
console.log('Opening invite modal');
|
console.log('Opening invite modal');
|
||||||
document.getElementById('invite-modal').classList.add('active');
|
document.getElementById('invite-modal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideInviteModal() {
|
function hideInviteModal() {
|
||||||
console.log('Closing invite modal');
|
console.log('Closing invite modal');
|
||||||
document.getElementById('invite-modal').classList.remove('active');
|
document.getElementById('invite-modal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAdminPanel() {
|
function showAdminPanel() {
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
showNotification('❌ Только для администратора', 'error');
|
showNotification('❌ Только для администратора', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Opening admin panel');
|
console.log('Opening admin panel');
|
||||||
document.getElementById('admin-modal').classList.add('active');
|
document.getElementById('admin-modal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideAdminModal() {
|
function hideAdminModal() {
|
||||||
console.log('Closing admin panel');
|
console.log('Closing admin panel');
|
||||||
document.getElementById('admin-modal').classList.remove('active');
|
document.getElementById('admin-modal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket обработчики - только если socket существует
|
// WebSocket обработчики - только если socket существует
|
||||||
if (typeof socket !== 'undefined') {
|
if (typeof socket !== 'undefined') {
|
||||||
socket.on('connect', function() {
|
socket.on('connect', function() {
|
||||||
console.log('Connected to WebSocket server');
|
console.log('Connected to WebSocket server');
|
||||||
if (roomCode) {
|
if (roomCode) {
|
||||||
@@ -573,21 +592,21 @@ if (typeof socket !== 'undefined') {
|
|||||||
location.reload();
|
location.reload();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('WebSocket не доступен, некоторые функции могут не работать');
|
console.warn('WebSocket не доступен, некоторые функции могут не работать');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функции управления комнатой
|
// Функции управления комнатой
|
||||||
function copyRoomCode() {
|
function copyRoomCode() {
|
||||||
navigator.clipboard.writeText('{{ room.code }}').then(() => {
|
navigator.clipboard.writeText('{{ room.code }}').then(() => {
|
||||||
showNotification('✅ Код комнаты скопирован');
|
showNotification('✅ Код комнаты скопирован');
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('Failed to copy: ', err);
|
console.error('Failed to copy: ', err);
|
||||||
showNotification('❌ Ошибка при копировании', 'error');
|
showNotification('❌ Ошибка при копировании', 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startGame() {
|
function startGame() {
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
showNotification('❌ Только администратор может начать игру', 'error');
|
showNotification('❌ Только администратор может начать игру', 'error');
|
||||||
return;
|
return;
|
||||||
@@ -614,15 +633,15 @@ function startGame() {
|
|||||||
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
|
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => response.json())
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showNotification('✅ Игра начинается...', 'success');
|
showNotification('✅ Игра начинается!', 'success');
|
||||||
|
|
||||||
|
// Ждем 2 секунды и перенаправляем в игру
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/game/{{ room.code }}';
|
||||||
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
showNotification('❌ Ошибка: ' + (data.error || 'Неизвестная ошибка'), 'error');
|
showNotification('❌ Ошибка: ' + (data.error || 'Неизвестная ошибка'), 'error');
|
||||||
}
|
}
|
||||||
@@ -631,13 +650,13 @@ function startGame() {
|
|||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showNotification('❌ Ошибка при запуске игры', 'error');
|
showNotification('❌ Ошибка при запуске игры', 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinGame() {
|
function joinGame() {
|
||||||
window.location.href = `/game/{{ room.code }}`;
|
window.location.href = `/game/{{ room.code }}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReady() {
|
function toggleReady() {
|
||||||
const isReady = document.getElementById('ready-btn').classList.contains('success');
|
const isReady = document.getElementById('ready-btn').classList.contains('success');
|
||||||
const newReadyState = !isReady;
|
const newReadyState = !isReady;
|
||||||
|
|
||||||
@@ -683,9 +702,9 @@ function toggleReady() {
|
|||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showNotification('❌ Ошибка при изменении статуса', 'error');
|
showNotification('❌ Ошибка при изменении статуса', 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRoomSettings(event) {
|
function updateRoomSettings(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const form = document.getElementById('room-settings-form');
|
const form = document.getElementById('room-settings-form');
|
||||||
@@ -719,9 +738,9 @@ function updateRoomSettings(event) {
|
|||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showNotification('❌ Ошибка при сохранении настроек', 'error');
|
showNotification('❌ Ошибка при сохранении настроек', 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyInviteLinkText() {
|
function copyInviteLinkText() {
|
||||||
const linkInput = document.getElementById('invite-link');
|
const linkInput = document.getElementById('invite-link');
|
||||||
linkInput.select();
|
linkInput.select();
|
||||||
linkInput.setSelectionRange(0, 99999);
|
linkInput.setSelectionRange(0, 99999);
|
||||||
@@ -732,10 +751,10 @@ function copyInviteLinkText() {
|
|||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
showNotification('✅ Ссылка скопирована', 'success');
|
showNotification('✅ Ссылка скопирована', 'success');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функции администрирования
|
// Функции администрирования
|
||||||
function kickPlayer(userId) {
|
function kickPlayer(userId) {
|
||||||
if (!confirm('Вы уверены, что хотите выгнать этого игрока?')) {
|
if (!confirm('Вы уверены, что хотите выгнать этого игрока?')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -759,9 +778,9 @@ function kickPlayer(userId) {
|
|||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showNotification('❌ Ошибка при выгонении игрока', 'error');
|
showNotification('❌ Ошибка при выгонении игрока', 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function kickAllPlayers() {
|
function kickAllPlayers() {
|
||||||
if (!confirm('ВЫ УВЕРЕНЫ? Это выгонит всех игроков из комнаты!')) {
|
if (!confirm('ВЫ УВЕРЕНЫ? Это выгонит всех игроков из комнаты!')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -785,9 +804,9 @@ function kickAllPlayers() {
|
|||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showNotification('❌ Ошибка при выгонении игроков', 'error');
|
showNotification('❌ Ошибка при выгонении игроков', 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetRoom() {
|
function resetRoom() {
|
||||||
if (!confirm('ВЫ УВЕРЕНЫ? Это сбросит комнату к начальному состоянию!')) {
|
if (!confirm('ВЫ УВЕРЕНЫ? Это сбросит комнату к начальному состоянию!')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -811,9 +830,9 @@ function resetRoom() {
|
|||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showNotification('❌ Ошибка при сбросе комнаты', 'error');
|
showNotification('❌ Ошибка при сбросе комнаты', 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteRoom() {
|
function deleteRoom() {
|
||||||
if (!confirm('ВЫ УВЕРЕНЫ? Это УДАЛИТ комнату навсегда!')) {
|
if (!confirm('ВЫ УВЕРЕНЫ? Это УДАЛИТ комнату навсегда!')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -839,15 +858,15 @@ function deleteRoom() {
|
|||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showNotification('❌ Ошибка при удалении комнаты', 'error');
|
showNotification('❌ Ошибка при удалении комнаты', 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функции чата
|
// Функции чата
|
||||||
function loadChatHistory() {
|
function loadChatHistory() {
|
||||||
const chatContainer = document.getElementById('chat-messages');
|
const chatContainer = document.getElementById('chat-messages');
|
||||||
chatContainer.innerHTML = '<div style="text-align: center; color: var(--light-text); padding: 20px;">Нет сообщений</div>';
|
chatContainer.innerHTML = '<div style="text-align: center; color: var(--light-text); padding: 20px;">Нет сообщений</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendMessage() {
|
function sendMessage() {
|
||||||
const input = document.getElementById('chat-input');
|
const input = document.getElementById('chat-input');
|
||||||
const message = input.value.trim();
|
const message = input.value.trim();
|
||||||
|
|
||||||
@@ -873,9 +892,9 @@ function sendMessage() {
|
|||||||
|
|
||||||
input.value = '';
|
input.value = '';
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addChatMessage(data) {
|
function addChatMessage(data) {
|
||||||
const chatContainer = document.getElementById('chat-messages');
|
const chatContainer = document.getElementById('chat-messages');
|
||||||
|
|
||||||
if (chatContainer.innerHTML.includes('Нет сообщений') || chatContainer.innerHTML.includes('Чат загружается')) {
|
if (chatContainer.innerHTML.includes('Нет сообщений') || chatContainer.innerHTML.includes('Чат загружается')) {
|
||||||
@@ -902,10 +921,10 @@ function addChatMessage(data) {
|
|||||||
|
|
||||||
chatContainer.appendChild(messageElement);
|
chatContainer.appendChild(messageElement);
|
||||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вспомогательные функции
|
// Вспомогательные функции
|
||||||
function updateReadyStatus() {
|
function updateReadyStatus() {
|
||||||
const readyPlayers = document.querySelectorAll('.player-ready.ready').length;
|
const readyPlayers = document.querySelectorAll('.player-ready.ready').length;
|
||||||
const totalPlayers = document.querySelectorAll('.player-item').length;
|
const totalPlayers = document.querySelectorAll('.player-item').length;
|
||||||
|
|
||||||
@@ -917,9 +936,9 @@ function updateReadyStatus() {
|
|||||||
const progress = totalPlayers > 0 ? (readyPlayers / totalPlayers * 100) : 0;
|
const progress = totalPlayers > 0 ? (readyPlayers / totalPlayers * 100) : 0;
|
||||||
readyProgress.style.width = `${progress}%`;
|
readyProgress.style.width = `${progress}%`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRoomStatus() {
|
function updateRoomStatus() {
|
||||||
fetch(`/api/room/{{ room.code }}/status`)
|
fetch(`/api/room/{{ room.code }}/status`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) return;
|
if (!response.ok) return;
|
||||||
@@ -931,9 +950,9 @@ function updateRoomStatus() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error updating room status:', error));
|
.catch(error => console.error('Error updating room status:', error));
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNotification(message, type = 'info') {
|
function showNotification(message, type = 'info') {
|
||||||
// Ограничиваем частоту уведомлений (не чаще 1 раза в 3 секунды)
|
// Ограничиваем частоту уведомлений (не чаще 1 раза в 3 секунды)
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastNotificationTime < 3000) {
|
if (now - lastNotificationTime < 3000) {
|
||||||
@@ -976,10 +995,10 @@ function showNotification(message, type = 'info') {
|
|||||||
notification.style.opacity = '0';
|
notification.style.opacity = '0';
|
||||||
setTimeout(() => notification.remove(), 300);
|
setTimeout(() => notification.remove(), 300);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработка клавиш
|
// Обработка клавиш
|
||||||
document.addEventListener('keydown', function(event) {
|
document.addEventListener('keydown', function(event) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
hideSettings();
|
hideSettings();
|
||||||
hideInviteModal();
|
hideInviteModal();
|
||||||
@@ -991,34 +1010,39 @@ document.addEventListener('keydown', function(event) {
|
|||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById('chat-input');
|
||||||
if (chatInput) chatInput.focus();
|
if (chatInput) chatInput.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Закрытие модальных окон при клике на фон
|
// Закрытие модальных окон при клике на фон
|
||||||
document.addEventListener('click', function(event) {
|
document.addEventListener('click', function(event) {
|
||||||
if (event.target.classList.contains('modal-backdrop')) {
|
if (event.target.classList.contains('modal-backdrop')) {
|
||||||
hideSettings();
|
hideSettings();
|
||||||
hideInviteModal();
|
hideInviteModal();
|
||||||
hideAdminModal();
|
hideAdminModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Фокус на поле ввода чата
|
// Фокус на поле ввода чата
|
||||||
const chatMessages = document.getElementById('chat-messages');
|
const chatMessages = document.getElementById('chat-messages');
|
||||||
if (chatMessages) {
|
if (chatMessages) {
|
||||||
chatMessages.addEventListener('click', function() {
|
chatMessages.addEventListener('click', function() {
|
||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById('chat-input');
|
||||||
if (chatInput) chatInput.focus();
|
if (chatInput) chatInput.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('beforeunload', function() {
|
window.addEventListener('beforeunload', function(e) {
|
||||||
|
// Только если WebSocket подключен и мы в комнате
|
||||||
if (typeof socket !== 'undefined' && socket.connected && roomCode) {
|
if (typeof socket !== 'undefined' && socket.connected && roomCode) {
|
||||||
|
// Не показываем подтверждение, если пользователь сам нажал выход
|
||||||
|
if (!window.userExitedLobby) {
|
||||||
|
// Отправляем событие выхода
|
||||||
socket.emit('leave_room', { room: roomCode });
|
socket.emit('leave_room', { room: roomCode });
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Обработка видимости страницы (если переключились на другую вкладку)
|
// Обработка видимости страницы (если переключились на другую вкладку)
|
||||||
document.addEventListener('visibilitychange', function() {
|
document.addEventListener('visibilitychange', function() {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
console.log('Page is now hidden');
|
console.log('Page is now hidden');
|
||||||
} else {
|
} else {
|
||||||
@@ -1026,10 +1050,10 @@ document.addEventListener('visibilitychange', function() {
|
|||||||
// При возвращении на страницу проверяем статус
|
// При возвращении на страницу проверяем статус
|
||||||
updateRoomStatus();
|
updateRoomStatus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Функция обновления списка игроков без перезагрузки страницы
|
// Функция обновления списка игроков без перезагрузки страницы
|
||||||
function updatePlayerList() {
|
function updatePlayerList() {
|
||||||
console.log('Updating player list...');
|
console.log('Updating player list...');
|
||||||
|
|
||||||
fetch(`/api/room/${roomCode}/players`)
|
fetch(`/api/room/${roomCode}/players`)
|
||||||
@@ -1047,10 +1071,10 @@ function updatePlayerList() {
|
|||||||
location.reload();
|
location.reload();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция обновления UI списка игроков
|
// Функция обновления UI списка игроков
|
||||||
function updatePlayerListUI(players) {
|
function updatePlayerListUI(players) {
|
||||||
const playerList = document.getElementById('player-list');
|
const playerList = document.getElementById('player-list');
|
||||||
if (!playerList) return;
|
if (!playerList) return;
|
||||||
|
|
||||||
@@ -1090,48 +1114,91 @@ function updatePlayerListUI(players) {
|
|||||||
|
|
||||||
playerList.appendChild(li);
|
playerList.appendChild(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработка дисконнекта WebSocket
|
// Обработка дисконнекта WebSocket
|
||||||
socket.on('disconnect', function(reason) {
|
socket.on('disconnect', function(reason) {
|
||||||
console.log('WebSocket disconnected:', reason);
|
console.log('WebSocket disconnected:', reason);
|
||||||
|
|
||||||
if (reason === 'io server disconnect') {
|
if (reason === 'io server disconnect') {
|
||||||
// Сервер намеренно отключил
|
// Сервер намеренно отключил
|
||||||
showNotification('🔌 Соединение с сервером потеряно. Переподключаемся...', 'warning');
|
showNotification('🔌 Соединение с сервером потеряно. Переподключаемся...', 'warning');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Периодическая проверка подключения
|
// Периодическая проверка подключения
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (socket && !socket.connected) {
|
if (socket && !socket.connected) {
|
||||||
console.log('Socket not connected, attempting to reconnect...');
|
console.log('Socket not connected, attempting to reconnect...');
|
||||||
socket.connect();
|
socket.connect();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
|
function exitLobby() {
|
||||||
|
if (!confirm('Вы уверены, что хотите выйти из лобби?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.userExitedLobby = true;
|
||||||
|
|
||||||
|
fetch('/room/{{ room.code }}/exit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showNotification('✅ Вы вышли из лобби', 'success');
|
||||||
|
|
||||||
|
// Отправляем WebSocket событие если подключены
|
||||||
|
if (typeof socket !== 'undefined' && socket.connected) {
|
||||||
|
socket.emit('leave_room', { room: roomCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Редирект через 1 секунду
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '{{ url_for("rooms") }}';
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
showNotification('❌ Ошибка: ' + (data.error || 'Неизвестная ошибка'), 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showNotification('❌ Ошибка при выходе из лобби', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.player-ready {
|
.player-ready {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-ready.ready {
|
.player-ready.ready {
|
||||||
background-color: #e8f5e9;
|
background-color: #e8f5e9;
|
||||||
color: #388e3c;
|
color: #388e3c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-ready.not-ready {
|
.player-ready.not-ready {
|
||||||
background-color: #ffebee;
|
background-color: #ffebee;
|
||||||
color: #d32f2f;
|
color: #d32f2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-ability {
|
.player-ability {
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -1143,25 +1210,25 @@ setInterval(() => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.danger {
|
.button.danger {
|
||||||
background-color: var(--danger-color);
|
background-color: var(--danger-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.danger:hover {
|
.button.danger:hover {
|
||||||
background-color: #c62828;
|
background-color: #c62828;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.warning {
|
.button.warning {
|
||||||
background-color: var(--warning-color);
|
background-color: var(--warning-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.warning:hover {
|
.button.warning:hover {
|
||||||
background-color: #f57c00;
|
background-color: #f57c00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -1172,13 +1239,13 @@ setInterval(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-backdrop.active {
|
.modal-backdrop.active {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -1187,9 +1254,9 @@ setInterval(() => {
|
|||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideInRight {
|
@keyframes slideInRight {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
@@ -1198,9 +1265,9 @@ setInterval(() => {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideOutRight {
|
@keyframes slideOutRight {
|
||||||
from {
|
from {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
@@ -1209,10 +1276,10 @@ setInterval(() => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Адаптивные стили */
|
/* Адаптивные стили */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.player-item {
|
.player-item {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -1231,6 +1298,69 @@ setInterval(() => {
|
|||||||
width: 95%;
|
width: 95%;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.button.danger {
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.danger:hover {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.danger:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exit-confirmation {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exit-confirmation.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exit-modal {
|
||||||
|
background-color: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exit-modal h3 {
|
||||||
|
color: var(--danger-color);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exit-modal p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exit-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exit-buttons .button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user