Aleksmorshen commited on
Commit
138b997
·
verified ·
1 Parent(s): 7645a07

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1728
app.py DELETED
@@ -1,1728 +0,0 @@
1
- from flask import Flask, render_template_string, request, redirect, url_for, session, jsonify
2
- import random
3
- import string
4
- import json
5
- import os
6
- from flask_socketio import SocketIO, join_room, leave_room, emit
7
- import hashlib
8
- import time
9
- import threading
10
- from datetime import datetime
11
- from huggingface_hub import HfApi, hf_hub_download
12
- from huggingface_hub.utils import RepositoryNotFoundError
13
- from urllib.parse import urlparse, parse_qs
14
-
15
- app = Flask(__name__)
16
- app.config['SECRET_KEY'] = 'your-very-secret-key-here'
17
- socketio = SocketIO(app, async_mode='threading')
18
-
19
- REPO_ID = "flpolprojects/Clients"
20
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
21
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
22
-
23
- ROOMS_DB = os.path.join(app.root_path, 'rooms.json')
24
- USERS_DB = os.path.join(app.root_path, 'users.json')
25
- GAMES_DB = os.path.join(app.root_path, 'games.json')
26
-
27
- def load_json(file_path, default={}):
28
- try:
29
- download_db_from_hf()
30
- if os.path.exists(file_path):
31
- with open(file_path, 'r', encoding='utf-8') as f:
32
- return json.load(f)
33
- return default
34
- except (FileNotFoundError, json.JSONDecodeError) as e:
35
- print(f"Ошибка загрузки JSON из {file_path}: {e}")
36
- return default
37
- except Exception as e:
38
- print(f"Непредвиденная ошибка при загрузке: {e}")
39
- return default
40
-
41
- def save_json(file_path, data):
42
- try:
43
- with open(file_path, 'w', encoding='utf-8') as f:
44
- json.dump(data, f, indent=4, ensure_ascii=False)
45
- upload_db_to_hf()
46
- except OSError as e:
47
- print(f"Ошибка сохранения JSON в {file_path}: {e}")
48
- except Exception as e:
49
- print(f"Непредвиденная ошибка при сохранении: {e}")
50
-
51
- def upload_db_to_hf():
52
- try:
53
- api = HfApi()
54
- for file_path, repo_path in [(ROOMS_DB, "rooms.json"), (USERS_DB, "users.json"), (GAMES_DB, "games.json")]:
55
- if os.path.exists(file_path):
56
- api.upload_file(
57
- path_or_fileobj=file_path,
58
- path_in_repo=repo_path,
59
- repo_id=REPO_ID,
60
- repo_type="dataset",
61
- token=HF_TOKEN_WRITE,
62
- commit_message=f"Backup: {repo_path} ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})"
63
- )
64
- except Exception as e:
65
- print(f"Ошибка при загрузке файлов на Hugging Face Hub: {e}")
66
-
67
- def download_db_from_hf():
68
- try:
69
- api = HfApi()
70
- for file_path, repo_path in [(ROOMS_DB, "rooms.json"), (USERS_DB, "users.json"), (GAMES_DB, "games.json")]:
71
- try:
72
- hf_hub_download(
73
- repo_id=REPO_ID,
74
- filename=repo_path,
75
- repo_type="dataset",
76
- token=HF_TOKEN_READ,
77
- local_dir=".",
78
- local_dir_use_symlinks=False
79
- )
80
- except RepositoryNotFoundError:
81
- if not os.path.exists(file_path):
82
- with open(file_path, 'w') as f:
83
- json.dump({}, f)
84
- except Exception as e:
85
- print(f"Ошибка при скачивании файла {repo_path}: {e}")
86
-
87
- except Exception as e:
88
- print(f"Ошибка при скачивании файлов с Hugging Face Hub: {e}")
89
-
90
- def periodic_backup():
91
- while True:
92
- upload_db_to_hf()
93
- time.sleep(15)
94
-
95
- rooms = load_json(ROOMS_DB)
96
- users = load_json(USERS_DB)
97
- games_data = load_json(GAMES_DB, default={
98
- "crocodile": {
99
- "name": "Крокодил",
100
- "description": "Один игрок показывает слово жестами.",
101
- "min_players": 2,
102
- "max_players": 10,
103
- "state": {}
104
- },
105
- "alias": {
106
- "name": "Alias",
107
- "description": "Один игрок объясняет слово.",
108
- "min_players": 2,
109
- "max_players": 10,
110
- "state": {}
111
- },
112
- "mafia": {
113
- "name": "Мафия",
114
- "description": "Мафия против мирных жителей.",
115
- "min_players": 4,
116
- "max_players": 10,
117
- "state": {}
118
- },
119
- "durak": {
120
- "name": "Дурак",
121
- "description": "Карточная игра.",
122
- "min_players": 2,
123
- "max_players": 6,
124
- "state": {}
125
- }
126
- })
127
- save_json(GAMES_DB, games_data)
128
-
129
-
130
- def generate_token():
131
- return ''.join(random.choices(string.ascii_letters + string.digits, k=15))
132
-
133
- def hash_password(password):
134
- return hashlib.sha256(password.encode('utf-8')).hexdigest()
135
-
136
- def get_youtube_id(url):
137
- if not url:
138
- return None
139
- parsed_url = urlparse(url)
140
- if parsed_url.hostname in ('www.youtube.com', 'youtube.com'):
141
- if parsed_url.path == '/watch':
142
- query = parse_qs(parsed_url.query)
143
- return query.get('v', [None])[0]
144
- elif parsed_url.path.startswith('/embed/'):
145
- return parsed_url.path.split('/embed/')[1].split('?')[0]
146
- elif parsed_url.path.startswith('/v/'):
147
- return parsed_url.path.split('/v/')[1].split('?')[0]
148
- elif parsed_url.hostname in ('youtu.be', 'www.youtu.be'):
149
- return parsed_url.path[1:].split('?')[0]
150
- return None
151
-
152
- @app.route('/', methods=['GET', 'POST'])
153
- def index():
154
- if 'username' in session:
155
- return redirect(url_for('dashboard'))
156
-
157
- if request.method == 'POST':
158
- action = request.form.get('action')
159
- username = request.form.get('username')
160
- password = request.form.get('password')
161
-
162
- if action == 'register':
163
- if username in users:
164
- return "Пользователь уже существует", 400
165
- users[username] = {'password': hash_password(password), 'rooms': []}
166
- save_json(USERS_DB, users)
167
- session['username'] = username
168
- return redirect(url_for('dashboard'))
169
-
170
- elif action == 'login':
171
- if username in users and users[username]['password'] == hash_password(password):
172
- session['username'] = username
173
- return redirect(url_for('dashboard'))
174
- return "Неверный логин или пароль", 401
175
-
176
- return render_template_string('''
177
- <!DOCTYPE html>
178
- <html lang="ru">
179
- <head>
180
- <meta charset="UTF-8">
181
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
182
- <title>Room</title>
183
- <style>
184
- :root {
185
- --primary-color: #6200ee;
186
- --secondary-color: #3700b3;
187
- --background-color: #ffffff;
188
- --surface-color: #f5f5f5;
189
- --text-color: #333333;
190
- --error-color: #b00020;
191
- --font-family: 'Roboto', sans-serif;
192
- --border-radius: 12px;
193
- --box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
194
- }
195
- body {
196
- font-family: var(--font-family);
197
- background-color: var(--background-color);
198
- color: var(--text-color);
199
- margin: 0;
200
- padding: 0;
201
- display: flex;
202
- justify-content: center;
203
- align-items: center;
204
- min-height: 100vh;
205
- }
206
- .container {
207
- background-color: var(--surface-color);
208
- padding: 2rem;
209
- border-radius: var(--border-radius);
210
- box-shadow: var(--box-shadow);
211
- width: 90%;
212
- max-width: 400px;
213
- text-align: center;
214
- }
215
- h1 {
216
- font-size: 2rem;
217
- margin-bottom: 1.5rem;
218
- color: var(--primary-color);
219
- }
220
- input, button {
221
- display: block;
222
- width: 100%;
223
- padding: 0.75rem;
224
- margin-bottom: 1rem;
225
- border: 1px solid #ccc;
226
- border-radius: var(--border-radius);
227
- font-size: 1rem;
228
- box-sizing: border-box;
229
- transition: border-color 0.3s ease;
230
- }
231
- input:focus {
232
- outline: none;
233
- border-color: var(--primary-color);
234
- }
235
- button {
236
- background-color: var(--primary-color);
237
- color: white;
238
- cursor: pointer;
239
- border: none;
240
- font-weight: 500;
241
- transition: background-color 0.3s ease;
242
- }
243
- button:hover {
244
- background-color: var(--secondary-color);
245
- }
246
- button:active {
247
- opacity: 0.8;
248
- }
249
- .error-message {
250
- color: var(--error-color);
251
- margin-top: 0.5rem;
252
- }
253
- @media (prefers-color-scheme: dark) {
254
- :root {
255
- --background-color: #121212;
256
- --surface-color: #1e1e1e;
257
- --text-color: #ffffff;
258
- }
259
- }
260
- </style>
261
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
262
- </head>
263
- <body>
264
- <div class="container">
265
- <h1>Room</h1>
266
- <form method="post">
267
- <input type="text" name="username" placeholder="Логин" required>
268
- <input type="password" name="password" placeholder="Пароль" required>
269
- <button type="submit" name="action" value="login">Войти</button>
270
- <button type="submit" name="action" value="register">Зарегистрироваться</button>
271
- </form>
272
- </div>
273
- </body>
274
- </html>
275
- ''')
276
-
277
-
278
- @app.route('/dashboard', methods=['GET', 'POST'])
279
- def dashboard():
280
- if 'username' not in session:
281
- return redirect(url_for('index'))
282
-
283
- user = users.get(session['username'])
284
- if not user:
285
- session.pop('username', None)
286
- return redirect(url_for('index'))
287
-
288
-
289
- if request.method == 'POST':
290
- action = request.form.get('action')
291
- if action == 'create':
292
- token = generate_token()
293
- rooms[token] = {'users': [session['username']], 'max_users': 10, 'admin': session['username'], 'current_game': None, 'guests': []}
294
- users[session['username']]['rooms'].append(token)
295
- save_json(ROOMS_DB, rooms)
296
- save_json(USERS_DB, users)
297
- return redirect(url_for('room', token=token))
298
- elif action == 'join':
299
- token = request.form.get('token')
300
- if token in rooms and len(rooms[token]['users']) + len(rooms[token]['guests']) < rooms[token]['max_users']:
301
- if session['username'] not in rooms[token]['users']:
302
- rooms[token]['users'].append(session['username'])
303
- users[session['username']]['rooms'].append(token)
304
- save_json(ROOMS_DB, rooms)
305
- save_json(USERS_DB, users)
306
- return redirect(url_for('room', token=token))
307
- return "Комната не найдена или переполнена", 404
308
-
309
- return render_template_string('''
310
- <!DOCTYPE html>
311
- <html lang="ru">
312
- <head>
313
- <meta charset="UTF-8">
314
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
315
- <title>Панель управления</title>
316
- <style>
317
- :root {
318
- --primary-color: #6200ee;
319
- --secondary-color: #3700b3;
320
- --background-color: #ffffff;
321
- --surface-color: #f5f5f5;
322
- --text-color: #333333;
323
- --accent-color: #03dac6;
324
- --font-family: 'Roboto', sans-serif;
325
- --border-radius: 12px;
326
- --box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
327
- }
328
- body {
329
- font-family: var(--font-family);
330
- background-color: var(--background-color);
331
- color: var(--text-color);
332
- margin: 0;
333
- padding: 0;
334
- display: flex;
335
- flex-direction: column;
336
- align-items: center;
337
- min-height: 100vh;
338
- }
339
- .container {
340
- background-color: var(--surface-color);
341
- padding: 2rem;
342
- border-radius: var(--border-radius);
343
- box-shadow: var(--box-shadow);
344
- width: 90%;
345
- max-width: 400px;
346
- text-align: center;
347
- margin-top: 2rem;
348
- }
349
- h1 {
350
- font-size: 2rem;
351
- margin-bottom: 1.5rem;
352
- color: var(--primary-color);
353
- }
354
- input, button {
355
- display: block;
356
- width: 100%;
357
- padding: 0.75rem;
358
- margin-bottom: 1rem;
359
- border: 1px solid #ccc;
360
- border-radius: var(--border-radius);
361
- font-size: 1rem;
362
- box-sizing: border-box;
363
- transition: border-color 0.3s ease;
364
- }
365
- input:focus {
366
- outline: none;
367
- border-color: var(--primary-color);
368
- }
369
- button {
370
- background-color: var(--primary-color);
371
- color: white;
372
- cursor: pointer;
373
- border: none;
374
- font-weight: 500;
375
- transition: background-color 0.3s ease;
376
- }
377
- button:hover {
378
- background-color: var(--secondary-color);
379
- }
380
- button:active {
381
- opacity: 0.8;
382
- }
383
- .logout-button {
384
- background-color: var(--accent-color);
385
- margin-top: 1rem;
386
- transition: background-color 0.3s ease;
387
- }
388
- .logout-button:hover {
389
- filter: brightness(0.9);
390
- }
391
- @media (prefers-color-scheme: dark) {
392
- :root {
393
- --background-color: #121212;
394
- --surface-color: #1e1e1e;
395
- --text-color: #ffffff;
396
- }
397
- }
398
- </style>
399
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
400
- </head>
401
- <body>
402
- <div class="container">
403
- <h1>Добро пожаловать, {{ session['username'] }}</h1>
404
- <form method="post">
405
- <button type="submit" name="action" value="create">Создать комнату</button>
406
- </form>
407
- <form method="post">
408
- <input type="text" name="token" placeholder="Введите токен комнаты" required>
409
- <button type="submit" name="action" value="join">Войти в комнату</button>
410
- </form>
411
- <form action="/logout" method="post">
412
- <button class="logout-button" type="submit">Выйти</button>
413
- </form>
414
- </div>
415
- </body>
416
- </html>
417
- ''', session=session)
418
-
419
-
420
- @app.route('/logout', methods=['POST'])
421
- def logout():
422
- session.pop('username', None)
423
- return redirect(url_for('index'))
424
-
425
-
426
- @app.route('/room/<token>')
427
- def room(token):
428
- if 'username' not in session and 'guest_id' not in session:
429
- return redirect(url_for('index'))
430
-
431
- if token not in rooms:
432
- return redirect(url_for('dashboard'))
433
-
434
- is_admin = rooms[token]['admin'] == session.get('username')
435
-
436
- if 'username' in session:
437
- username = session['username']
438
- is_guest = False
439
- elif 'guest_id' in session:
440
- username = session['guest_id']
441
- is_guest = True
442
- if username not in rooms[token]['guests']:
443
- rooms[token]['guests'].append(username)
444
- save_json(ROOMS_DB, rooms)
445
- else:
446
- return redirect(url_for('guest_login', token=token))
447
-
448
- return render_template_string('''
449
- <!DOCTYPE html>
450
- <html lang="ru">
451
- <head>
452
- <meta charset="UTF-8">
453
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
454
- <title>Метавселенная - Комната {{ token }}</title>
455
- <style>
456
- body {
457
- margin: 0;
458
- overflow: hidden;
459
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
460
- background-color: #111;
461
- color: #fff;
462
- }
463
- canvas {
464
- display: block;
465
- }
466
- #blocker {
467
- position: absolute;
468
- width: 100%;
469
- height: 100%;
470
- background-color: rgba(0,0,0,0.7);
471
- display: flex;
472
- justify-content: center;
473
- align-items: center;
474
- text-align: center;
475
- font-size: 24px;
476
- cursor: pointer;
477
- }
478
- #instructions {
479
- width: 50%;
480
- background: rgba(20,20,20,0.8);
481
- padding: 20px;
482
- border-radius: 10px;
483
- }
484
- .video-chat-overlay {
485
- position: absolute;
486
- top: 10px;
487
- right: 10px;
488
- width: 250px;
489
- background: rgba(0,0,0,0.5);
490
- border-radius: 8px;
491
- padding: 5px;
492
- display: flex;
493
- flex-direction: column;
494
- gap: 5px;
495
- max-height: 90vh;
496
- overflow-y: auto;
497
- }
498
- .video-container {
499
- position: relative;
500
- }
501
- video {
502
- width: 100%;
503
- border-radius: 5px;
504
- }
505
- .user-indicator {
506
- position: absolute;
507
- bottom: 5px;
508
- left: 5px;
509
- background: rgba(0,0,0,0.6);
510
- color: white;
511
- padding: 2px 5px;
512
- border-radius: 3px;
513
- font-size: 12px;
514
- }
515
- #crosshair {
516
- position: absolute;
517
- top: 50%;
518
- left: 50%;
519
- width: 10px;
520
- height: 10px;
521
- border: 1px solid white;
522
- border-radius: 50%;
523
- transform: translate(-50%, -50%);
524
- }
525
- .hotspot-prompt {
526
- position: absolute;
527
- bottom: 20%;
528
- left: 50%;
529
- transform: translateX(-50%);
530
- background: rgba(0,0,0,0.7);
531
- padding: 10px 20px;
532
- border-radius: 5px;
533
- font-size: 16px;
534
- display: none;
535
- }
536
- .modal-overlay {
537
- position: absolute;
538
- top: 0;
539
- left: 0;
540
- width: 100%;
541
- height: 100%;
542
- background: rgba(0,0,0,0.8);
543
- display: none;
544
- justify-content: center;
545
- align-items: center;
546
- z-index: 1000;
547
- }
548
- .modal-content {
549
- background: #222;
550
- padding: 20px;
551
- border-radius: 10px;
552
- max-width: 90%;
553
- max-height: 90%;
554
- overflow-y: auto;
555
- position: relative;
556
- }
557
- .modal-close {
558
- position: absolute;
559
- top: 10px;
560
- right: 15px;
561
- font-size: 24px;
562
- cursor: pointer;
563
- }
564
- #youtube-player-container {
565
- width: 80vw;
566
- height: 45vw;
567
- max-width: 1280px;
568
- max-height: 720px;
569
- }
570
- #game-display {
571
- padding: 20px;
572
- background-color: #333;
573
- border-radius: 10px;
574
- text-align: center;
575
- width: 80vw;
576
- max-width: 600px;
577
- }
578
- .game-input, .game-button {
579
- padding: 10px;
580
- margin: 5px;
581
- border-radius: 5px;
582
- border: 1px solid #555;
583
- background: #444;
584
- color: #fff;
585
- }
586
- #mobile-controls {
587
- position: absolute;
588
- bottom: 0;
589
- left: 0;
590
- width: 100%;
591
- height: 40%;
592
- display: none;
593
- z-index: 10;
594
- }
595
- #joystick-zone {
596
- position: absolute;
597
- bottom: 20px;
598
- left: 20px;
599
- width: 120px;
600
- height: 120px;
601
- background: rgba(255,255,255,0.2);
602
- border-radius: 50%;
603
- }
604
- #joystick-thumb {
605
- position: absolute;
606
- width: 60px;
607
- height: 60px;
608
- background: rgba(255,255,255,0.4);
609
- border-radius: 50%;
610
- top: 30px;
611
- left: 30px;
612
- }
613
- #look-zone {
614
- position: absolute;
615
- bottom: 0;
616
- right: 0;
617
- width: 50%;
618
- height: 100%;
619
- }
620
- .leave-button {
621
- position: absolute;
622
- top: 20px;
623
- left: 20px;
624
- padding: 10px 15px;
625
- background: #f44336;
626
- color: white;
627
- border: none;
628
- border-radius: 5px;
629
- cursor: pointer;
630
- z-index: 20;
631
- }
632
- </style>
633
- </head>
634
- <body>
635
- <div id="blocker">
636
- <div id="instructions">
637
- <p style="font-size: 36px;">Нажмите, чтобы войти в метавселенную</p>
638
- <p>Движение: W, A, S, D<br/>Осмотр: Мышь<br/>Взаимодействие: E</p>
639
- <p>На мобильных устройствах используйте джойстик и правую часть экрана</p>
640
- </div>
641
- </div>
642
-
643
- <div class="video-chat-overlay" id="video-grid"></div>
644
- <div id="crosshair">+</div>
645
- <div class="hotspot-prompt" id="hotspot-prompt">Нажмите [E] для взаимодействия</div>
646
-
647
- <div class="modal-overlay" id="youtube-modal">
648
- <div class="modal-content">
649
- <span class="modal-close" id="youtube-close">×</span>
650
- <div id="youtube-player-container"></div>
651
- </div>
652
- </div>
653
-
654
- <div class="modal-overlay" id="info-modal">
655
- <div class="modal-content">
656
- <span class="modal-close" id="info-close">×</span>
657
- <p id="info-text"></p>
658
- </div>
659
- </div>
660
-
661
- <div class="modal-overlay" id="game-modal">
662
- <div class="modal-content">
663
- <span class="modal-close" id="game-close">×</span>
664
- <div id="game-display">
665
- <h2></h2>
666
- <p id="game-description"></p>
667
- <div id="game-content"></div>
668
- </div>
669
- </div>
670
- </div>
671
-
672
- <div id="mobile-controls">
673
- <div id="joystick-zone">
674
- <div id="joystick-thumb"></div>
675
- </div>
676
- <div id="look-zone"></div>
677
- </div>
678
-
679
- <button class="leave-button" onclick="leaveRoom()">Покинуть комнату</button>
680
-
681
- <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script>
682
- <script src="https://www.youtube.com/iframe_api"></script>
683
- <script type="importmap">
684
- {
685
- "imports": {
686
- "three": "https://unpkg.com/three@0.165.0/build/three.module.js",
687
- "three/addons/": "https://unpkg.com/three@0.165.0/examples/jsm/"
688
- }
689
- }
690
- </script>
691
- <script type="module">
692
- import * as THREE from 'three';
693
- import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
694
-
695
- const socket = io();
696
- const token = '{{ token }}';
697
- const username = '{{ username }}';
698
- const is_guest = {{ is_guest|tojson }};
699
- const isAdmin = {{ is_admin|tojson }};
700
-
701
- let camera, scene, renderer, controls;
702
- const objects = [];
703
- let raycaster;
704
-
705
- let moveForward = false;
706
- let moveBackward = false;
707
- let moveLeft = false;
708
- let moveRight = false;
709
- let canJump = false;
710
-
711
- let prevTime = performance.now();
712
- const velocity = new THREE.Vector3();
713
- const direction = new THREE.Vector3();
714
-
715
- const players = {};
716
- let localStream;
717
- const peers = {};
718
- const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
719
-
720
- const hotspots = [
721
- { pos: new THREE.Vector3(5, 1.5, -10), type: 'youtube', data: 'dQw4w9WgXcQ', name: 'Секретное видео' },
722
- { pos: new THREE.Vector3(-8, 1.5, -8), type: 'info', data: 'Добро пожаловать в нашу метавселенную! Исследуйте мир и общайтесь с друзьями.', name: 'Приветствие' },
723
- { pos: new THREE.Vector3(0, 1.5, 10), type: 'game', data: 'crocodile', name: 'Играть в Крокодила' }
724
- ];
725
- let currentHotspot = null;
726
- let youtubePlayer;
727
-
728
- const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
729
-
730
- init();
731
- animate();
732
-
733
- function init() {
734
- camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
735
- camera.position.y = 1.6;
736
-
737
- scene = new THREE.Scene();
738
- scene.background = new THREE.Color(0x87ceeb);
739
- scene.fog = new THREE.Fog(0x87ceeb, 0, 75);
740
-
741
- const light = new THREE.HemisphereLight(0xeeeeff, 0x777788, 0.9);
742
- light.position.set(0.5, 1, 0.75);
743
- scene.add(light);
744
-
745
- const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
746
- dirLight.position.set(-1, 1.75, 1);
747
- dirLight.position.multiplyScalar(30);
748
- scene.add(dirLight);
749
-
750
- raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, -1, 0), 0, 10);
751
-
752
- const mapSize = 20;
753
- const floorGeometry = new THREE.PlaneGeometry(mapSize*2, mapSize*2, 100, 100);
754
- const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x4a7d2c });
755
- const floor = new THREE.Mesh(floorGeometry, floorMaterial);
756
- floor.rotation.x = -Math.PI / 2;
757
- scene.add(floor);
758
-
759
- const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
760
- const mapLayout = [
761
- "####################",
762
- "# #",
763
- "# #### #### #",
764
- "# # # # # #",
765
- "# #### #### #",
766
- "# #",
767
- "# ###### #",
768
- "# # # #",
769
- "# # # #",
770
- "# ###### #",
771
- "# #",
772
- "# #",
773
- "# #### #### #",
774
- "# # # # # #",
775
- "# # # # # #",
776
- "# #### #### #",
777
- "# #",
778
- "# #",
779
- "# #",
780
- "####################",
781
- ];
782
-
783
- for(let z=0; z<mapLayout.length; z++) {
784
- for(let x=0; x<mapLayout[z].length; x++) {
785
- if (mapLayout[z][x] === '#') {
786
- const wallMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
787
- const wall = new THREE.Mesh(boxGeometry, wallMaterial);
788
- wall.position.set(x - mapSize/2 + 0.5, 0.5, z - mapSize/2 + 0.5);
789
- scene.add(wall);
790
- objects.push(wall);
791
- const wallTop = new THREE.Mesh(boxGeometry, wallMaterial);
792
- wallTop.position.set(x - mapSize/2 + 0.5, 1.5, z - mapSize/2 + 0.5);
793
- scene.add(wallTop);
794
- objects.push(wallTop);
795
- }
796
- }
797
- }
798
-
799
- hotspots.forEach(hotspot => {
800
- const hotspotGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.1, 32);
801
- const hotspotMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.7 });
802
- const hotspotMesh = new THREE.Mesh(hotspotGeometry, hotspotMaterial);
803
- hotspotMesh.position.copy(hotspot.pos);
804
- scene.add(hotspotMesh);
805
- hotspot.mesh = hotspotMesh;
806
- });
807
-
808
- renderer = new THREE.WebGLRenderer({ antialias: true });
809
- renderer.setPixelRatio(window.devicePixelRatio);
810
- renderer.setSize(window.innerWidth, window.innerHeight);
811
- document.body.appendChild(renderer.domElement);
812
-
813
- if (isMobile) {
814
- initMobileControls();
815
- } else {
816
- initPointerLock();
817
- }
818
-
819
- document.addEventListener('keydown', onKeyDown);
820
- document.addEventListener('keyup', onKeyUp);
821
- window.addEventListener('resize', onWindowResize);
822
-
823
- document.getElementById('hotspot-prompt').addEventListener('click', interactWithHotspot);
824
- document.getElementById('youtube-close').addEventListener('click', closeYoutubeModal);
825
- document.getElementById('info-close').addEventListener('click', closeInfoModal);
826
- document.getElementById('game-close').addEventListener('click', closeGameModal);
827
- }
828
-
829
- function initPointerLock() {
830
- const blocker = document.getElementById('blocker');
831
- const instructions = document.getElementById('instructions');
832
- controls = new PointerLockControls(camera, document.body);
833
-
834
- instructions.addEventListener('click', function () {
835
- controls.lock();
836
- });
837
-
838
- controls.addEventListener('lock', function () {
839
- instructions.style.display = 'none';
840
- blocker.style.display = 'none';
841
- });
842
-
843
- controls.addEventListener('unlock', function () {
844
- blocker.style.display = 'flex';
845
- instructions.style.display = '';
846
- });
847
- scene.add(controls.getObject());
848
- }
849
-
850
- function initMobileControls() {
851
- document.getElementById('blocker').addEventListener('click', () => {
852
- document.getElementById('blocker').style.display = 'none';
853
- });
854
- document.getElementById('mobile-controls').style.display = 'block';
855
-
856
- const joystickZone = document.getElementById('joystick-zone');
857
- const joystickThumb = document.getElementById('joystick-thumb');
858
- const lookZone = document.getElementById('look-zone');
859
-
860
- let joystickActive = false;
861
- let joystickStart = { x: 0, y: 0 };
862
- const joystickCenter = { x: joystickZone.offsetLeft + 60, y: joystickZone.offsetTop + 60 };
863
-
864
- joystickZone.addEventListener('touchstart', (e) => {
865
- e.preventDefault();
866
- joystickActive = true;
867
- joystickStart.x = e.touches[0].clientX;
868
- joystickStart.y = e.touches[0].clientY;
869
- }, { passive: false });
870
-
871
- joystickZone.addEventListener('touchmove', (e) => {
872
- e.preventDefault();
873
- if (!joystickActive) return;
874
- let dx = e.touches[0].clientX - joystickStart.x;
875
- let dy = e.touches[0].clientY - joystickStart.y;
876
- let dist = Math.sqrt(dx * dx + dy * dy);
877
- let angle = Math.atan2(dy, dx);
878
-
879
- if (dist > 30) {
880
- dx = Math.cos(angle) * 30;
881
- dy = Math.sin(angle) * 30;
882
- }
883
-
884
- joystickThumb.style.transform = `translate(${dx}px, ${dy}px)`;
885
-
886
- moveForward = dy > 10;
887
- moveBackward = dy < -10;
888
- moveLeft = dx < -10;
889
- moveRight = dx > 10;
890
- }, { passive: false });
891
-
892
- joystickZone.addEventListener('touchend', (e) => {
893
- joystickActive = false;
894
- joystickThumb.style.transform = 'translate(0,0)';
895
- moveForward = moveBackward = moveLeft = moveRight = false;
896
- });
897
-
898
- let lookActive = false;
899
- let lookStart = { x: 0, y: 0 };
900
-
901
- lookZone.addEventListener('touchstart', (e) => {
902
- e.preventDefault();
903
- lookActive = true;
904
- lookStart.x = e.touches[0].clientX;
905
- lookStart.y = e.touches[0].clientY;
906
- }, { passive: false });
907
-
908
- lookZone.addEventListener('touchmove', (e) => {
909
- e.preventDefault();
910
- if (!lookActive) return;
911
- let dx = e.touches[0].clientX - lookStart.x;
912
- let dy = e.touches[0].clientY - lookStart.y;
913
-
914
- camera.rotation.y -= dx * 0.002;
915
- camera.rotation.x -= dy * 0.002;
916
- camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, camera.rotation.x));
917
-
918
- lookStart.x = e.touches[0].clientX;
919
- lookStart.y = e.touches[0].clientY;
920
-
921
- }, { passive: false });
922
-
923
- lookZone.addEventListener('touchend', () => {
924
- lookActive = false;
925
- });
926
- }
927
-
928
- function onWindowResize() {
929
- camera.aspect = window.innerWidth / window.innerHeight;
930
- camera.updateProjectionMatrix();
931
- renderer.setSize(window.innerWidth, window.innerHeight);
932
- }
933
-
934
- function onKeyDown(event) {
935
- switch (event.code) {
936
- case 'ArrowUp':
937
- case 'KeyW':
938
- moveForward = true;
939
- break;
940
- case 'ArrowLeft':
941
- case 'KeyA':
942
- moveLeft = true;
943
- break;
944
- case 'ArrowDown':
945
- case 'KeyS':
946
- moveBackward = true;
947
- break;
948
- case 'ArrowRight':
949
- case 'KeyD':
950
- moveRight = true;
951
- break;
952
- case 'Space':
953
- if (canJump === true) velocity.y += 350;
954
- canJump = false;
955
- break;
956
- case 'KeyE':
957
- interactWithHotspot();
958
- break;
959
- }
960
- }
961
-
962
- function onKeyUp(event) {
963
- switch (event.code) {
964
- case 'ArrowUp':
965
- case 'KeyW':
966
- moveForward = false;
967
- break;
968
- case 'ArrowLeft':
969
- case 'KeyA':
970
- moveLeft = false;
971
- break;
972
- case 'ArrowDown':
973
- case 'KeyS':
974
- moveBackward = false;
975
- break;
976
- case 'ArrowRight':
977
- case 'KeyD':
978
- moveRight = false;
979
- break;
980
- }
981
- }
982
-
983
- function animate() {
984
- requestAnimationFrame(animate);
985
- const time = performance.now();
986
-
987
- let isMoving = false;
988
- if (isMobile || controls.isLocked === true) {
989
- const delta = (time - prevTime) / 1000;
990
-
991
- velocity.x -= velocity.x * 10.0 * delta;
992
- velocity.z -= velocity.z * 10.0 * delta;
993
- velocity.y -= 9.8 * 100.0 * delta;
994
-
995
- direction.z = Number(moveForward) - Number(moveBackward);
996
- direction.x = Number(moveRight) - Number(moveLeft);
997
- direction.normalize();
998
-
999
- if (moveForward || moveBackward) velocity.z -= direction.z * 400.0 * delta;
1000
- if (moveLeft || moveRight) velocity.x -= direction.x * 400.0 * delta;
1001
-
1002
- if (isMobile) {
1003
- camera.translateX(velocity.x * delta);
1004
- camera.translateZ(velocity.z * delta);
1005
- } else {
1006
- controls.moveRight(-velocity.x * delta);
1007
- controls.moveForward(-velocity.z * delta);
1008
- }
1009
-
1010
- if (Math.abs(velocity.x) > 0.1 || Math.abs(velocity.z) > 0.1) {
1011
- isMoving = true;
1012
- }
1013
-
1014
- if (isMobile) {
1015
- camera.position.y += (velocity.y * delta);
1016
- if (camera.position.y < 1.6) {
1017
- velocity.y = 0;
1018
- camera.position.y = 1.6;
1019
- canJump = true;
1020
- }
1021
- } else {
1022
- controls.getObject().position.y += (velocity.y * delta);
1023
- if (controls.getObject().position.y < 1.6) {
1024
- velocity.y = 0;
1025
- controls.getObject().position.y = 1.6;
1026
- canJump = true;
1027
- }
1028
- }
1029
-
1030
- checkHotspots();
1031
- }
1032
-
1033
- prevTime = time;
1034
-
1035
- for (const id in players) {
1036
- if (players[id].mesh && players[id].target) {
1037
- players[id].mesh.position.lerp(players[id].target.pos, 0.1);
1038
- players[id].mesh.quaternion.slerp(players[id].target.quat, 0.1);
1039
- }
1040
- }
1041
-
1042
- renderer.render(scene, camera);
1043
- }
1044
-
1045
- function checkHotspots() {
1046
- let playerPos = isMobile ? camera.position : controls.getObject().position;
1047
- let closestHotspot = null;
1048
- let minDistance = 2;
1049
-
1050
- hotspots.forEach(hotspot => {
1051
- const distance = playerPos.distanceTo(hotspot.pos);
1052
- if (distance < minDistance) {
1053
- closestHotspot = hotspot;
1054
- minDistance = distance;
1055
- }
1056
- });
1057
-
1058
- const prompt = document.getElementById('hotspot-prompt');
1059
- if (closestHotspot) {
1060
- prompt.style.display = 'block';
1061
- prompt.textContent = `Нажмите [E] чтобы: ${closestHotspot.name}`;
1062
- currentHotspot = closestHotspot;
1063
- } else {
1064
- prompt.style.display = 'none';
1065
- currentHotspot = null;
1066
- }
1067
- }
1068
-
1069
- function interactWithHotspot() {
1070
- if (!currentHotspot) return;
1071
- if(!isMobile) controls.unlock();
1072
-
1073
- switch(currentHotspot.type) {
1074
- case 'youtube':
1075
- openYoutubeModal(currentHotspot.data);
1076
- break;
1077
- case 'info':
1078
- openInfoModal(currentHotspot.data);
1079
- break;
1080
- case 'game':
1081
- if (isAdmin) {
1082
- openGameModal(currentHotspot.data);
1083
- } else {
1084
- openInfoModal('Только администратор может начать игру.');
1085
- }
1086
- break;
1087
- }
1088
- }
1089
-
1090
- function openYoutubeModal(videoId) {
1091
- document.getElementById('youtube-modal').style.display = 'flex';
1092
- if (youtubePlayer) {
1093
- youtubePlayer.loadVideoById(videoId);
1094
- } else {
1095
- youtubePlayer = new YT.Player('youtube-player-container', {
1096
- height: '100%',
1097
- width: '100%',
1098
- videoId: videoId,
1099
- events: { 'onReady': (e) => e.target.playVideo() }
1100
- });
1101
- }
1102
- }
1103
-
1104
- function closeYoutubeModal() {
1105
- document.getElementById('youtube-modal').style.display = 'none';
1106
- if (youtubePlayer && typeof youtubePlayer.stopVideo === 'function') {
1107
- youtubePlayer.stopVideo();
1108
- }
1109
- }
1110
-
1111
- function openInfoModal(text) {
1112
- document.getElementById('info-text').textContent = text;
1113
- document.getElementById('info-modal').style.display = 'flex';
1114
- }
1115
-
1116
- function closeInfoModal() {
1117
- document.getElementById('info-modal').style.display = 'none';
1118
- }
1119
-
1120
- function openGameModal(gameId) {
1121
- document.getElementById('game-modal').style.display = 'flex';
1122
- startGame(gameId);
1123
- }
1124
-
1125
- function closeGameModal() {
1126
- document.getElementById('game-modal').style.display = 'none';
1127
- }
1128
-
1129
- setInterval(() => {
1130
- if (isMobile || (controls && controls.isLocked)) {
1131
- let playerPos = isMobile ? camera.position : controls.getObject().position;
1132
- let playerRot = isMobile ? camera.quaternion : controls.getObject().quaternion;
1133
- socket.emit('player_move', {
1134
- token: token,
1135
- username: username,
1136
- pos: playerPos.toArray(),
1137
- quat: playerRot.toArray()
1138
- });
1139
- }
1140
- }, 100);
1141
-
1142
- function addPlayer(id) {
1143
- if (players[id] || id === username) return;
1144
- const playerGroup = new THREE.Group();
1145
-
1146
- const bodyMaterial = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff });
1147
- const body = new THREE.Mesh(new THREE.BoxGeometry(0.8, 1.2, 0.5), bodyMaterial);
1148
- body.position.y = 0.6;
1149
-
1150
- const headMaterial = new THREE.MeshStandardMaterial({ color: 0xffdbac });
1151
- const head = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.4), headMaterial);
1152
- head.position.y = 1.4;
1153
-
1154
- const nameCanvas = document.createElement('canvas');
1155
- const context = nameCanvas.getContext('2d');
1156
- context.font = "Bold 40px Arial";
1157
- context.fillStyle = "rgba(255,255,255,0.95)";
1158
- context.fillText(id, 0, 40);
1159
- const nameTexture = new THREE.CanvasTexture(nameCanvas);
1160
- const nameSpriteMaterial = new THREE.SpriteMaterial({ map: nameTexture });
1161
- const nameSprite = new THREE.Sprite(nameSpriteMaterial);
1162
- nameSprite.position.y = 2.0;
1163
- nameSprite.scale.set(2, 1, 1);
1164
-
1165
- playerGroup.add(body);
1166
- playerGroup.add(head);
1167
- playerGroup.add(nameSprite);
1168
-
1169
- scene.add(playerGroup);
1170
- players[id] = { mesh: playerGroup, target: { pos: new THREE.Vector3(), quat: new THREE.Quaternion() } };
1171
- }
1172
-
1173
- function removePlayer(id) {
1174
- if (players[id]) {
1175
- scene.remove(players[id].mesh);
1176
- delete players[id];
1177
- }
1178
- }
1179
-
1180
- socket.on('connect', () => {
1181
- console.log('Connected to server!');
1182
- if (username || is_guest) {
1183
- socket.emit('join', { token, username, is_guest });
1184
- }
1185
- });
1186
-
1187
- socket.on('player_moved', (data) => {
1188
- if (data.username === username) return;
1189
- if (!players[data.username]) {
1190
- addPlayer(data.username);
1191
- }
1192
- if(players[data.username]) {
1193
- players[data.username].target.pos.fromArray(data.pos);
1194
- players[data.username].target.quat.fromArray(data.quat);
1195
- }
1196
- });
1197
-
1198
- socket.on('init_users', (data) => {
1199
- const allUsers = [...data.users, ...data.guests];
1200
- allUsers.forEach(user => addPlayer(user));
1201
- });
1202
-
1203
- socket.on('user_joined', (data) => {
1204
- addPlayer(data.username);
1205
- });
1206
-
1207
- socket.on('user_left', (data) => {
1208
- removePlayer(data.username);
1209
- });
1210
-
1211
- navigator.mediaDevices.getUserMedia({ video: true, audio: true })
1212
- .then(stream => {
1213
- localStream = stream;
1214
- addVideoStream(stream, username, true);
1215
- })
1216
- .catch(err => {
1217
- console.error("Ошибка доступа к медиаустройствам:", err);
1218
- });
1219
-
1220
- function addVideoStream(stream, user, isLocal = false) {
1221
- const videoGrid = document.getElementById('video-grid');
1222
- const videoContainer = document.createElement('div');
1223
- videoContainer.classList.add('video-container');
1224
-
1225
- const video = document.createElement('video');
1226
- video.srcObject = stream;
1227
- video.dataset.user = user;
1228
- video.setAttribute('playsinline', '');
1229
- video.setAttribute('autoplay', '');
1230
- if (isLocal) video.muted = true;
1231
-
1232
- const nameTag = document.createElement('div');
1233
- nameTag.classList.add('user-indicator');
1234
- nameTag.textContent = user;
1235
-
1236
- videoContainer.appendChild(video);
1237
- videoContainer.appendChild(nameTag);
1238
- videoGrid.appendChild(videoContainer);
1239
- video.play();
1240
- }
1241
-
1242
- socket.on('signal', data => {
1243
- if (data.from === username) return;
1244
- handleSignal(data.from, data.signal);
1245
- });
1246
-
1247
- function handleSignal(from, signal) {
1248
- if (!peers[from]) {
1249
- createPeerConnection(from, false);
1250
- }
1251
- peers[from].signal(signal);
1252
- }
1253
-
1254
- function createPeerConnection(remoteUser, initiator) {
1255
- const peer = new SimplePeer({
1256
- initiator: initiator,
1257
- trickle: false,
1258
- stream: localStream,
1259
- config: iceConfig
1260
- });
1261
-
1262
- peer.on('signal', signal => {
1263
- socket.emit('signal', { to: remoteUser, from: username, token, signal });
1264
- });
1265
-
1266
- peer.on('stream', stream => {
1267
- addVideoStream(stream, remoteUser);
1268
- });
1269
-
1270
- peer.on('close', () => {
1271
- const videoElement = document.querySelector(`video[data-user="${remoteUser}"]`);
1272
- if (videoElement) videoElement.parentElement.remove();
1273
- delete peers[remoteUser];
1274
- });
1275
-
1276
- peers[remoteUser] = peer;
1277
- }
1278
-
1279
- function leaveRoom() {
1280
- socket.emit('leave', { token, username, is_guest });
1281
- window.location.href = is_guest ? '/guest_login/' + token : '/dashboard';
1282
- }
1283
- window.leaveRoom = leaveRoom;
1284
-
1285
-
1286
- // Game Logic Integration
1287
- function startGame(gameId) {
1288
- console.log('Starting game:', gameId);
1289
- document.getElementById('game-display').style.display = 'block';
1290
- document.getElementById('game-content').innerHTML = '';
1291
- socket.emit('start_game', { token, game_id: gameId });
1292
- }
1293
- window.startGame = startGame;
1294
-
1295
- socket.on('game_started', (data) => {
1296
- const gameId = data.game_id;
1297
- const gameInfo = {{ games_data|tojson }}[gameId];
1298
- const gameDisplay = document.getElementById('game-display');
1299
- gameDisplay.style.display = 'block';
1300
- document.getElementById('game-description').innerText = gameInfo.description;
1301
- gameDisplay.querySelector('h2').innerText = gameInfo.name;
1302
-
1303
- const gameContent = document.getElementById('game-content');
1304
- gameContent.innerHTML = '';
1305
-
1306
- if (gameId === 'crocodile' || gameId === 'alias'){
1307
- initWordGame(gameId, gameContent);
1308
- } else if(gameId === 'mafia'){
1309
- initMafia(gameContent, gameInfo);
1310
- }
1311
- });
1312
-
1313
- function initWordGame(gameId, gameContent) {
1314
- let elements = `
1315
- <input type="text" id="${gameId}-word-input" placeholder="Введите слово (только админ)" class="game-input">
1316
- <button id="start-turn-button" class="game-button">Начать ход</button>
1317
- <input type="text" id="${gameId}-guess-input" placeholder="Ваша догадка" class="game-input">
1318
- <button id="${gameId}-guess-button" class="game-button">Угадать</button>
1319
- <div id="${gameId}-result"></div>
1320
- <div id="${gameId}-timer"></div>
1321
- `;
1322
- gameContent.innerHTML = elements;
1323
-
1324
- const wordInput = document.getElementById(`${gameId}-word-input`);
1325
- const startTurnButton = document.getElementById('start-turn-button');
1326
- const guessInput = document.getElementById(`${gameId}-guess-input`);
1327
- const guessButton = document.getElementById(`${gameId}-guess-button`);
1328
-
1329
- wordInput.style.display = isAdmin ? 'block' : 'none';
1330
- startTurnButton.style.display = isAdmin ? 'block' : 'none';
1331
-
1332
- startTurnButton.onclick = () => {
1333
- const word = wordInput.value.trim();
1334
- if (word) {
1335
- socket.emit('set_game_state', {token, game_id: gameId, state: { word: word, presenter: null, guesses: [], timer: 60, isRunning: true }});
1336
- wordInput.value = '';
1337
- }
1338
- };
1339
-
1340
- guessButton.onclick = () => {
1341
- const guess = guessInput.value.trim();
1342
- if (guess) {
1343
- socket.emit('game_action', { token, game_id: gameId, action: 'guess', value: guess, user: username });
1344
- guessInput.value = '';
1345
- }
1346
- };
1347
- }
1348
-
1349
- function initMafia(gameContent, gameInfo) {
1350
- let elements = `
1351
- <button id="start-mafia-button" class="game-button">Начать игру (только админ)</button>
1352
- <div id="mafia-result"></div>
1353
- <input type="text" id="mafia-vote-input" placeholder="За кого голосуете?" class="game-input">
1354
- <button id="mafia-vote-button" class="game-button">Голосовать</button>
1355
- `;
1356
- gameContent.innerHTML = elements;
1357
-
1358
- const startButton = document.getElementById('start-mafia-button');
1359
- const voteInput = document.getElementById('mafia-vote-input');
1360
- const voteButton = document.getElementById('mafia-vote-button');
1361
-
1362
- startButton.style.display = isAdmin ? 'block' : 'none';
1363
- startButton.onclick = () => socket.emit('set_game_state', { token, game_id: 'mafia', state: { roles: {}, phase: 'night', votes: {}, isRunning: true, killed: null } });
1364
-
1365
- voteButton.onclick = () => {
1366
- const vote = voteInput.value.trim();
1367
- if (vote) {
1368
- socket.emit('game_action', { token, game_id: 'mafia', action: 'vote', value: vote, user: username });
1369
- voteInput.value = '';
1370
- }
1371
- };
1372
- }
1373
-
1374
- socket.on('update_game_state', (data) => {
1375
- const gameId = data.game_id;
1376
- const gameState = data.state;
1377
-
1378
- if (gameId === 'crocodile' || gameId === 'alias'){
1379
- updateWordGameState(gameId, gameState);
1380
- } else if (gameId === 'mafia') {
1381
- updateMafiaState(gameState);
1382
- }
1383
- });
1384
-
1385
- function updateWordGameState(gameId, gameState) {
1386
- const resultDiv = document.getElementById(`${gameId}-result`);
1387
- const timerDiv = document.getElementById(`${gameId}-timer`);
1388
- const guessInput = document.getElementById(`${gameId}-guess-input`);
1389
- const guessButton = document.getElementById(`${gameId}-guess-button`);
1390
- if (!resultDiv) return;
1391
-
1392
- resultDiv.innerHTML = '';
1393
- if(gameState.presenter) {
1394
- resultDiv.innerHTML += `<p>Ведущий: ${gameState.presenter}</p>`;
1395
- }
1396
- if (username === gameState.presenter) {
1397
- resultDiv.innerHTML += `<p>Ваше слово: <strong>${gameState.word}</strong></p>`;
1398
- }
1399
- gameState.guesses.forEach(g => {
1400
- resultDiv.innerHTML += `<p>${g.user}: ${g.value} (${g.result})</p>`;
1401
- });
1402
- timerDiv.textContent = `Время: ${gameState.timer}`;
1403
-
1404
- const isGameOver = !gameState.isRunning || gameState.timer <= 0;
1405
- guessInput.disabled = isGameOver || username === gameState.presenter;
1406
- guessButton.disabled = isGameOver || username === gameState.presenter;
1407
-
1408
- if (isGameOver && gameState.word) {
1409
- resultDiv.innerHTML += `<p>Игра окончена! Загаданное слово было: <strong>${gameState.word}</strong></p>`;
1410
- }
1411
- }
1412
-
1413
- function updateMafiaState(gameState) {
1414
- const resultDiv = document.getElementById('mafia-result');
1415
- const voteInput = document.getElementById('mafia-vote-input');
1416
- const voteButton = document.getElementById('mafia-vote-button');
1417
- if (!resultDiv) return;
1418
-
1419
- resultDiv.innerHTML = '';
1420
- if (gameState.roles && gameState.roles[username]) {
1421
- resultDiv.innerHTML += `<p>Ваша роль: ${gameState.roles[username]}</p>`;
1422
- }
1423
- resultDiv.innerHTML += `<p>Фаза: ${gameState.phase === 'day' ? 'День' : 'Ночь'}</p>`;
1424
- if (gameState.phase === 'day') {
1425
- if (gameState.killed) {
1426
- resultDiv.innerHTML += `<p>Ночью был убит: ${gameState.killed}</p>`;
1427
- }
1428
- resultDiv.innerHTML += `<p>Идет голосование. Живые игроки: ${gameState.players.join(', ')}</p>`;
1429
- voteInput.disabled = false;
1430
- voteButton.disabled = false;
1431
- } else {
1432
- resultDiv.innerHTML += `<p>Мафия выбирает жертву. Мирные спят.</p>`;
1433
- voteInput.disabled = true;
1434
- voteButton.disabled = true;
1435
- }
1436
- if (gameState.winner) {
1437
- resultDiv.innerHTML += `<h2>Игра окончена! Победили: ${gameState.winner}</h2>`;
1438
- }
1439
- }
1440
- </script>
1441
- </body>
1442
- </html>
1443
- ''', token=token, session=session, is_admin=is_admin, games_data=games_data, username=username, is_guest=is_guest)
1444
-
1445
-
1446
- @app.route('/join_as_guest/<token>', methods=['GET'])
1447
- def join_as_guest(token):
1448
- if token not in rooms:
1449
- return "Комната не найдена", 404
1450
-
1451
- guest_id = 'Гость_' + ''.join(random.choices(string.digits, k=4))
1452
- session['guest_id'] = guest_id
1453
-
1454
- if 'username' in session:
1455
- session.pop('username')
1456
-
1457
- return redirect(url_for('room', token=token))
1458
-
1459
- @app.route('/guest_login/<token>')
1460
- def guest_login(token):
1461
- if token not in rooms:
1462
- return "Комната не найдена", 404
1463
-
1464
- return render_template_string('''
1465
- <!DOCTYPE html>
1466
- <html lang="ru">
1467
- <head>
1468
- <meta charset="UTF-8">
1469
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1470
- <title>Вход для гостей</title>
1471
- <style>
1472
- :root {
1473
- --primary-color: #4CAF50;
1474
- --background-color: #f0f0f0;
1475
- --surface-color: #ffffff;
1476
- --text-color: #333333;
1477
- --border-radius: 12px;
1478
- --box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.15);
1479
- }
1480
- body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: var(--background-color); color: var(--text-color); }
1481
- .container { text-align: center; padding: 30px; border: 1px solid #ccc; border-radius: var(--border-radius); background-color: var(--surface-color); box-shadow: var(--box-shadow); }
1482
- h1 { color: var(--primary-color); margin-bottom: 20px; }
1483
- p { margin-bottom: 20px; }
1484
- a { padding: 12px 25px; background-color: var(--primary-color); color: white; text-decoration: none; border-radius: 5px; font-size: 1.1rem; transition: background-color 0.3s ease; }
1485
- a:hover { background-color: #388E3C; }
1486
- </style>
1487
- </head>
1488
- <body>
1489
- <div class="container">
1490
- <h1>Вход в комнату как Гость</h1>
1491
- <p>Вы можете войти в комнату <strong>{{ token }}</strong> как гость. Ваше имя будет автоматически сгенерировано.</p>
1492
- <a href="{{ url_for('join_as_guest', token=token) }}">Войти в комнату как гость</a>
1493
- <p style="margin-top: 20px;"><a href="{{ url_for('index') }}">Или войти/зарегистрироваться</a></p>
1494
- </div>
1495
- </body>
1496
- </html>
1497
- ''', token=token)
1498
-
1499
-
1500
- @socketio.on('connect')
1501
- def handle_connect():
1502
- print(f"Client connected: {request.sid}")
1503
-
1504
- @socketio.on('disconnect')
1505
- def handle_disconnect_custom():
1506
- print(f"Client disconnected: {request.sid}")
1507
-
1508
- @socketio.on('join')
1509
- def handle_join(data):
1510
- token = data['token']
1511
- username = data['username']
1512
- join_room(token)
1513
-
1514
- if token not in rooms:
1515
- return
1516
-
1517
- all_users = rooms[token]['users'] + rooms[token]['guests']
1518
- emit('init_users', {'users': rooms[token]['users'], 'guests': rooms[token]['guests']}, to=request.sid)
1519
-
1520
- emit('user_joined', {
1521
- 'username': username,
1522
- 'users': rooms[token]['users'],
1523
- 'guests': rooms[token]['guests']
1524
- }, room=token, include_self=False)
1525
-
1526
- if rooms[token]['current_game']:
1527
- game_id = rooms[token]['current_game']
1528
- emit('game_started', {'game_id': game_id}, to=request.sid)
1529
- if token in games_data.get(game_id, {}).get('state', {}):
1530
- emit('update_game_state', {'game_id': game_id, 'state': games_data[game_id]['state'][token]}, to=request.sid)
1531
-
1532
- @socketio.on('leave')
1533
- def handle_leave(data):
1534
- token = data['token']
1535
- username = data['username']
1536
- is_guest = data.get('is_guest', False)
1537
- leave_room(token)
1538
-
1539
- if token in rooms:
1540
- if is_guest:
1541
- if username in rooms[token]['guests']:
1542
- rooms[token]['guests'].remove(username)
1543
- else:
1544
- if username in rooms[token]['users']:
1545
- rooms[token]['users'].remove(username)
1546
-
1547
- if not rooms[token]['users'] and not rooms[token]['guests']:
1548
- del rooms[token]
1549
- else:
1550
- emit('user_left', {'username': username}, room=token)
1551
-
1552
- save_json(ROOMS_DB, rooms)
1553
-
1554
- @socketio.on('player_move')
1555
- def handle_player_move(data):
1556
- token = data['token']
1557
- if token in rooms:
1558
- emit('player_moved', {
1559
- 'username': data['username'],
1560
- 'pos': data['pos'],
1561
- 'quat': data['quat']
1562
- }, room=token, include_self=False)
1563
-
1564
- @socketio.on('signal')
1565
- def handle_signal(data):
1566
- if data['token'] in rooms:
1567
- emit('signal', {
1568
- 'from': data['from'],
1569
- 'signal': data['signal']
1570
- }, room=data['to'])
1571
-
1572
-
1573
- @socketio.on('start_game')
1574
- def handle_start_game(data):
1575
- token = data['token']
1576
- game_id = data['game_id']
1577
- username = session.get('username')
1578
-
1579
- if token in rooms and rooms[token].get('admin') == username:
1580
- all_users_in_room = rooms[token]['users'] + rooms[token]['guests']
1581
- min_p = games_data[game_id]['min_players']
1582
- max_p = games_data[game_id]['max_players']
1583
-
1584
- if not (min_p <= len(all_users_in_room) <= max_p):
1585
- return
1586
-
1587
- rooms[token]['current_game'] = game_id
1588
- if game_id not in games_data: games_data[game_id] = {'state': {}}
1589
- elif 'state' not in games_data[game_id]: games_data[game_id]['state'] = {}
1590
-
1591
- games_data[game_id]['state'][token] = {}
1592
- save_json(ROOMS_DB, rooms)
1593
- save_json(GAMES_DB, games_data)
1594
- emit('game_started', {'game_id': game_id}, room=token)
1595
-
1596
-
1597
- @socketio.on('set_game_state')
1598
- def handle_set_game_state(data):
1599
- token = data['token']
1600
- game_id = data['game_id']
1601
- state = data['state']
1602
- username = session.get('username')
1603
-
1604
- if token in rooms and rooms[token].get('admin') == username:
1605
- if rooms[token]['current_game'] == game_id:
1606
- all_users = rooms[token]['users'] + rooms[token]['guests']
1607
- state['players'] = all_users
1608
-
1609
- if (game_id == 'crocodile' or game_id == 'alias') and 'presenter' not in state:
1610
- state['presenter'] = random.choice(all_users) if all_users else None
1611
-
1612
- if game_id == 'mafia' and not state.get('roles'):
1613
- state['roles'] = assign_mafia_roles(all_users)
1614
-
1615
- games_data[game_id]['state'][token] = state
1616
- save_json(GAMES_DB, games_data)
1617
- emit('update_game_state', {'game_id': game_id, 'state': state}, room=token)
1618
-
1619
- if state.get('isRunning') and (game_id == 'crocodile' or game_id == 'alias'):
1620
- start_timer(token, game_id)
1621
-
1622
- def assign_mafia_roles(users):
1623
- num_players = len(users)
1624
- num_mafia = 1 if num_players < 6 else 2
1625
- roles_list = ['mafia'] * num_mafia + ['civilian'] * (num_players - num_mafia)
1626
- random.shuffle(roles_list)
1627
- return {user: role for user, role in zip(users, roles_list)}
1628
-
1629
- @socketio.on('game_action')
1630
- def handle_game_action(data):
1631
- token = data['token']
1632
- game_id = data['game_id']
1633
- action = data['action']
1634
- user = data['user']
1635
- value = data.get('value')
1636
-
1637
- if token not in rooms or game_id != rooms[token].get('current_game'): return
1638
- current_state = games_data[game_id]['state'].get(token, {})
1639
- if not current_state.get('isRunning'): return
1640
-
1641
- if game_id in ['crocodile', 'alias'] and action == 'guess':
1642
- if user == current_state.get('presenter'): return
1643
- result = "Угадано!" if value.lower() == current_state.get('word', '').lower() else "Не угадано"
1644
- current_state.setdefault('guesses', []).append({'user': user, 'value': value, 'result': result})
1645
- if result == "Угадано!": current_state['isRunning'] = False
1646
-
1647
- elif game_id == 'mafia' and action == 'vote':
1648
- if current_state['phase'] == 'day':
1649
- current_state.setdefault('votes', {})[user] = value
1650
- all_players = current_state.get('players', [])
1651
- if len(current_state['votes']) == len(all_players):
1652
- process_mafia_votes(current_state)
1653
-
1654
- games_data[game_id]['state'][token] = current_state
1655
- save_json(GAMES_DB, games_data)
1656
- emit('update_game_state', {'game_id': game_id, 'state': current_state}, room=token)
1657
-
1658
- def process_mafia_votes(state):
1659
- votes = state.get('votes', {})
1660
- counts = {}
1661
- for vote in votes.values(): counts[vote] = counts.get(vote, 0) + 1
1662
-
1663
- max_votes = 0
1664
- voted_out = []
1665
- for player, num_votes in counts.items():
1666
- if num_votes > max_votes:
1667
- max_votes = num_votes
1668
- voted_out = [player]
1669
- elif num_votes == max_votes:
1670
- voted_out.append(player)
1671
-
1672
- if len(voted_out) == 1:
1673
- player_to_remove = voted_out[0]
1674
- state['players'].remove(player_to_remove)
1675
- state['killed'] = player_to_remove
1676
-
1677
- remaining_mafia = sum(1 for p in state['players'] if state['roles'].get(p) == 'mafia')
1678
- remaining_civilians = len(state['players']) - remaining_mafia
1679
-
1680
- if remaining_mafia == 0:
1681
- state['winner'] = 'Мирные'
1682
- state['isRunning'] = False
1683
- elif remaining_mafia >= remaining_civilians:
1684
- state['winner'] = 'Мафия'
1685
- state['isRunning'] = False
1686
-
1687
- state['phase'] = 'night'
1688
- state['votes'] = {}
1689
-
1690
-
1691
- def start_timer(token, game_id):
1692
- def timer_loop():
1693
- with app.app_context():
1694
- while True:
1695
- state = games_data[game_id]['state'].get(token)
1696
- if not state or not state.get('isRunning') or state.get('timer', 0) <= 0:
1697
- if state:
1698
- state['isRunning'] = False
1699
- games_data[game_id]['state'][token] = state
1700
- save_json(GAMES_DB, games_data)
1701
- socketio.emit('update_game_state', {'game_id': game_id, 'state': state}, room=token)
1702
- break
1703
-
1704
- state['timer'] -= 1
1705
- games_data[game_id]['state'][token] = state
1706
- save_json(GAMES_DB, games_data)
1707
- socketio.emit('update_game_state', {'game_id': game_id, 'state': state}, room=token)
1708
- socketio.sleep(1)
1709
- socketio.start_background_task(timer_loop)
1710
-
1711
- if __name__ == '__main__':
1712
- if not os.path.exists(os.path.join(app.root_path, 'rooms.json')):
1713
- with open(os.path.join(app.root_path, 'rooms.json'), 'w') as f: json.dump({}, f)
1714
- if not os.path.exists(os.path.join(app.root_path, 'users.json')):
1715
- with open(os.path.join(app.root_path, 'users.json'), 'w') as f: json.dump({}, f)
1716
- if not os.path.exists(os.path.join(app.root_path, 'games.json')):
1717
- save_json(GAMES_DB, games_data)
1718
-
1719
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1720
- #backup_thread.start()
1721
-
1722
- # ВАЖНО: для работы WebRTC нужен HTTPS. Werkzeug создает самоподписанный сертификат.
1723
- # Вам нужно будет разрешить его в браузере (на странице "Дополнительно" -> "Перейти на сайт (небезопасно)").
1724
- # Для продакшена используйте полноценный веб-сервер (Nginx) с настоящими SSL-сертификатами.
1725
- try:
1726
- socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True, ssl_context='adhoc')
1727
- except TypeError:
1728
- socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True)