Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -13,140 +13,76 @@ 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 |
-
|
| 21 |
-
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 22 |
|
| 23 |
-
|
| 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
|
| 28 |
try:
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
except Exception as e:
|
| 38 |
-
print(f"
|
| 39 |
-
return default
|
| 40 |
|
| 41 |
-
def save_json(file_path, data):
|
| 42 |
try:
|
| 43 |
-
with open(
|
| 44 |
-
json.
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
print(f"Ошибка сохранения JSON в {file_path}: {e}")
|
| 48 |
-
except Exception as e:
|
| 49 |
-
print(f"Непредвиденная ошибка при сохранении: {e}")
|
| 50 |
|
| 51 |
-
def
|
| 52 |
try:
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
except Exception as e:
|
| 87 |
-
print(f"Ошибка при скачивании файлов с Hugging Face Hub: {e}")
|
| 88 |
|
| 89 |
def periodic_backup():
|
| 90 |
while True:
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
rooms = load_json(ROOMS_DB)
|
| 95 |
-
users = load_json(USERS_DB)
|
| 96 |
-
games_data = load_json(GAMES_DB, default={
|
| 97 |
-
"crocodile": {
|
| 98 |
-
"name": "Крокодил",
|
| 99 |
-
"description": "Один игрок показывает слово жестами.",
|
| 100 |
-
"min_players": 2,
|
| 101 |
-
"max_players": 10,
|
| 102 |
-
"state": {}
|
| 103 |
-
},
|
| 104 |
-
"alias": {
|
| 105 |
-
"name": "Alias",
|
| 106 |
-
"description": "Один игрок объясняет слово.",
|
| 107 |
-
"min_players": 2,
|
| 108 |
-
"max_players": 10,
|
| 109 |
-
"state": {}
|
| 110 |
-
},
|
| 111 |
-
"mafia": {
|
| 112 |
-
"name": "Мафия",
|
| 113 |
-
"description": "Мафия против мирных жителей.",
|
| 114 |
-
"min_players": 4,
|
| 115 |
-
"max_players": 10,
|
| 116 |
-
"state": {}
|
| 117 |
-
},
|
| 118 |
-
"durak": {
|
| 119 |
-
"name": "Дурак",
|
| 120 |
-
"description": "Карточная игра.",
|
| 121 |
-
"min_players": 2,
|
| 122 |
-
"max_players": 6,
|
| 123 |
-
"state": {}
|
| 124 |
-
}
|
| 125 |
-
})
|
| 126 |
-
save_json(GAMES_DB, games_data)
|
| 127 |
|
| 128 |
def generate_token():
|
| 129 |
-
return ''.join(random.choices(string.ascii_letters + string.digits, k=
|
| 130 |
|
| 131 |
def hash_password(password):
|
| 132 |
return hashlib.sha256(password.encode('utf-8')).hexdigest()
|
| 133 |
|
| 134 |
-
def get_youtube_id(url):
|
| 135 |
-
if not url:
|
| 136 |
-
return None
|
| 137 |
-
parsed_url = urlparse(url)
|
| 138 |
-
if parsed_url.hostname in ('www.youtube.com', 'youtube.com'):
|
| 139 |
-
if parsed_url.path == '/watch':
|
| 140 |
-
query = parse_qs(parsed_url.query)
|
| 141 |
-
return query.get('v', [None])[0]
|
| 142 |
-
elif parsed_url.path.startswith('/embed/'):
|
| 143 |
-
return parsed_url.path.split('/embed/')[1].split('?')[0]
|
| 144 |
-
elif parsed_url.path.startswith('/v/'):
|
| 145 |
-
return parsed_url.path.split('/v/')[1].split('?')[0]
|
| 146 |
-
elif parsed_url.hostname in ('youtu.be', 'www.youtu.be'):
|
| 147 |
-
return parsed_url.path[1:].split('?')[0]
|
| 148 |
-
return None
|
| 149 |
-
|
| 150 |
@app.route('/', methods=['GET', 'POST'])
|
| 151 |
def index():
|
| 152 |
if 'username' in session:
|
|
@@ -158,15 +94,16 @@ def index():
|
|
| 158 |
password = request.form.get('password')
|
| 159 |
|
| 160 |
if action == 'register':
|
| 161 |
-
if username in users:
|
| 162 |
return "Пользователь уже существует", 400
|
| 163 |
-
users[username] = {'password': hash_password(password)
|
| 164 |
-
|
| 165 |
session['username'] = username
|
| 166 |
return redirect(url_for('dashboard'))
|
| 167 |
|
| 168 |
elif action == 'login':
|
| 169 |
-
|
|
|
|
| 170 |
session['username'] = username
|
| 171 |
return redirect(url_for('dashboard'))
|
| 172 |
return "Неверный логин или пароль", 401
|
|
@@ -177,25 +114,22 @@ def index():
|
|
| 177 |
<head>
|
| 178 |
<meta charset="UTF-8">
|
| 179 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 180 |
-
<title>
|
| 181 |
<style>
|
| 182 |
:root {
|
| 183 |
-
--primary-color: #
|
| 184 |
-
--secondary-color: #
|
| 185 |
-
--background-color: #
|
| 186 |
-
--surface-color: #
|
| 187 |
-
--text-color: #
|
| 188 |
-
--error-color: #b00020;
|
| 189 |
--font-family: 'Roboto', sans-serif;
|
| 190 |
--border-radius: 12px;
|
| 191 |
-
--box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
| 192 |
}
|
| 193 |
body {
|
| 194 |
font-family: var(--font-family);
|
| 195 |
background-color: var(--background-color);
|
| 196 |
color: var(--text-color);
|
| 197 |
margin: 0;
|
| 198 |
-
padding: 0;
|
| 199 |
display: flex;
|
| 200 |
justify-content: center;
|
| 201 |
align-items: center;
|
|
@@ -205,107 +139,88 @@ def index():
|
|
| 205 |
background-color: var(--surface-color);
|
| 206 |
padding: 2rem;
|
| 207 |
border-radius: var(--border-radius);
|
| 208 |
-
box-shadow:
|
| 209 |
width: 90%;
|
| 210 |
max-width: 400px;
|
| 211 |
text-align: center;
|
| 212 |
}
|
| 213 |
h1 {
|
| 214 |
-
font-size:
|
| 215 |
margin-bottom: 1.5rem;
|
| 216 |
color: var(--primary-color);
|
|
|
|
| 217 |
}
|
| 218 |
input, button {
|
| 219 |
display: block;
|
| 220 |
width: 100%;
|
| 221 |
-
padding: 0.
|
| 222 |
-
margin-bottom:
|
| 223 |
-
border: 1px solid #
|
|
|
|
|
|
|
| 224 |
border-radius: var(--border-radius);
|
| 225 |
font-size: 1rem;
|
| 226 |
box-sizing: border-box;
|
| 227 |
-
transition:
|
| 228 |
}
|
| 229 |
input:focus {
|
| 230 |
outline: none;
|
| 231 |
border-color: var(--primary-color);
|
|
|
|
| 232 |
}
|
| 233 |
button {
|
| 234 |
background-color: var(--primary-color);
|
| 235 |
-
color: white;
|
| 236 |
cursor: pointer;
|
| 237 |
border: none;
|
| 238 |
font-weight: 500;
|
| 239 |
-
transition: background-color 0.3s ease;
|
| 240 |
}
|
| 241 |
button:hover {
|
| 242 |
background-color: var(--secondary-color);
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
opacity: 0.8;
|
| 246 |
-
}
|
| 247 |
-
.error-message {
|
| 248 |
-
color: var(--error-color);
|
| 249 |
-
margin-top: 0.5rem;
|
| 250 |
-
}
|
| 251 |
-
@media (prefers-color-scheme: dark) {
|
| 252 |
-
:root {
|
| 253 |
-
--background-color: #121212;
|
| 254 |
-
--surface-color: #1e1e1e;
|
| 255 |
-
--text-color: #ffffff;
|
| 256 |
-
}
|
| 257 |
}
|
| 258 |
</style>
|
| 259 |
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
| 260 |
</head>
|
| 261 |
<body>
|
| 262 |
<div class="container">
|
| 263 |
-
<h1>
|
| 264 |
<form method="post">
|
| 265 |
<input type="text" name="username" placeholder="Логин" required>
|
| 266 |
<input type="password" name="password" placeholder="Пароль" required>
|
| 267 |
<button type="submit" name="action" value="login">Войти</button>
|
| 268 |
-
<button type="submit" name="action" value="register"
|
| 269 |
</form>
|
| 270 |
</div>
|
| 271 |
</body>
|
| 272 |
</html>
|
| 273 |
''')
|
| 274 |
|
| 275 |
-
|
| 276 |
@app.route('/dashboard', methods=['GET', 'POST'])
|
| 277 |
def dashboard():
|
| 278 |
if 'username' not in session:
|
| 279 |
return redirect(url_for('index'))
|
| 280 |
|
| 281 |
-
user = users.get(session['username'])
|
| 282 |
-
if not user:
|
| 283 |
-
session.pop('username', None)
|
| 284 |
-
return redirect(url_for('index'))
|
| 285 |
-
|
| 286 |
if request.method == 'POST':
|
| 287 |
action = request.form.get('action')
|
| 288 |
if action == 'create':
|
| 289 |
token = generate_token()
|
| 290 |
-
rooms[token] = {
|
| 291 |
-
'players': {},
|
| 292 |
'admin': session['username'],
|
| 293 |
-
'
|
| 294 |
-
'
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
| 296 |
}
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
save_json(USERS_DB, users)
|
| 300 |
-
return redirect(url_for('room', token=token))
|
| 301 |
elif action == 'join':
|
| 302 |
token = request.form.get('token')
|
| 303 |
-
if token in rooms:
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
save_json(USERS_DB, users)
|
| 307 |
-
return redirect(url_for('room', token=token))
|
| 308 |
-
return "Комната не найдена", 404
|
| 309 |
|
| 310 |
return render_template_string('''
|
| 311 |
<!DOCTYPE html>
|
|
@@ -316,22 +231,21 @@ def dashboard():
|
|
| 316 |
<title>Панель управления</title>
|
| 317 |
<style>
|
| 318 |
:root {
|
| 319 |
-
--primary-color: #
|
| 320 |
-
--secondary-color: #
|
| 321 |
-
--background-color: #
|
| 322 |
-
--surface-color: #
|
| 323 |
-
--text-color: #
|
| 324 |
-
--accent-color: #
|
| 325 |
--font-family: 'Roboto', sans-serif;
|
| 326 |
--border-radius: 12px;
|
| 327 |
-
--box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
| 328 |
}
|
| 329 |
body {
|
| 330 |
font-family: var(--font-family);
|
| 331 |
background-color: var(--background-color);
|
| 332 |
color: var(--text-color);
|
| 333 |
margin: 0;
|
| 334 |
-
padding:
|
| 335 |
display: flex;
|
| 336 |
flex-direction: column;
|
| 337 |
align-items: center;
|
|
@@ -341,11 +255,10 @@ def dashboard():
|
|
| 341 |
background-color: var(--surface-color);
|
| 342 |
padding: 2rem;
|
| 343 |
border-radius: var(--border-radius);
|
| 344 |
-
box-shadow:
|
| 345 |
width: 90%;
|
| 346 |
-
max-width:
|
| 347 |
text-align: center;
|
| 348 |
-
margin-top: 2rem;
|
| 349 |
}
|
| 350 |
h1 {
|
| 351 |
font-size: 2rem;
|
|
@@ -355,46 +268,36 @@ def dashboard():
|
|
| 355 |
input, button {
|
| 356 |
display: block;
|
| 357 |
width: 100%;
|
| 358 |
-
padding: 0.
|
| 359 |
margin-bottom: 1rem;
|
| 360 |
-
border: 1px solid #
|
|
|
|
|
|
|
| 361 |
border-radius: var(--border-radius);
|
| 362 |
font-size: 1rem;
|
| 363 |
box-sizing: border-box;
|
| 364 |
-
transition:
|
| 365 |
}
|
| 366 |
input:focus {
|
| 367 |
outline: none;
|
| 368 |
border-color: var(--primary-color);
|
|
|
|
| 369 |
}
|
| 370 |
button {
|
| 371 |
background-color: var(--primary-color);
|
| 372 |
-
color: white;
|
| 373 |
cursor: pointer;
|
| 374 |
border: none;
|
| 375 |
font-weight: 500;
|
| 376 |
-
transition: background-color 0.3s ease;
|
| 377 |
}
|
| 378 |
button:hover {
|
| 379 |
background-color: var(--secondary-color);
|
| 380 |
-
|
| 381 |
-
button:active {
|
| 382 |
-
opacity: 0.8;
|
| 383 |
}
|
| 384 |
.logout-button {
|
| 385 |
background-color: var(--accent-color);
|
| 386 |
-
margin-top: 1rem;
|
| 387 |
-
transition: background-color 0.3s ease;
|
| 388 |
}
|
| 389 |
.logout-button:hover {
|
| 390 |
-
|
| 391 |
-
}
|
| 392 |
-
@media (prefers-color-scheme: dark) {
|
| 393 |
-
:root {
|
| 394 |
-
--background-color: #121212;
|
| 395 |
-
--surface-color: #1e1e1e;
|
| 396 |
-
--text-color: #ffffff;
|
| 397 |
-
}
|
| 398 |
}
|
| 399 |
</style>
|
| 400 |
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
|
@@ -403,673 +306,782 @@ def dashboard():
|
|
| 403 |
<div class="container">
|
| 404 |
<h1>Добро пожаловать, {{ session['username'] }}</h1>
|
| 405 |
<form method="post">
|
| 406 |
-
<button type="submit" name="action" value="create">Создать
|
| 407 |
</form>
|
|
|
|
| 408 |
<form method="post">
|
| 409 |
-
<input type="text" name="token" placeholder="Введите
|
| 410 |
-
<button type="submit" name="action" value="join">Войти в
|
| 411 |
</form>
|
| 412 |
-
<form action="/logout" method="post">
|
| 413 |
<button class="logout-button" type="submit">Выйти</button>
|
| 414 |
</form>
|
| 415 |
</div>
|
| 416 |
</body>
|
| 417 |
</html>
|
| 418 |
-
'''
|
| 419 |
|
| 420 |
@app.route('/logout', methods=['POST'])
|
| 421 |
def logout():
|
| 422 |
session.pop('username', None)
|
| 423 |
return redirect(url_for('index'))
|
| 424 |
|
| 425 |
-
@app.route('/
|
| 426 |
-
def
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
if token not in rooms:
|
| 432 |
return redirect(url_for('dashboard'))
|
| 433 |
|
| 434 |
-
|
| 435 |
-
is_admin =
|
| 436 |
|
| 437 |
return render_template_string('''
|
| 438 |
<!DOCTYPE html>
|
| 439 |
<html lang="ru">
|
| 440 |
<head>
|
| 441 |
<meta charset="UTF-8">
|
| 442 |
-
<
|
| 443 |
-
<
|
| 444 |
-
<
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
--font-family: 'Roboto', sans-serif;
|
| 452 |
-
--border-radius: 12px;
|
| 453 |
-
--box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.3);
|
| 454 |
-
}
|
| 455 |
-
body { margin: 0; overflow: hidden; font-family: var(--font-family); }
|
| 456 |
-
.hud { position: fixed; top: 0; left: 0; width: 100%; z-index: 10; padding: 10px; box-sizing: border-box; display: flex; justify-content: space-between; align-items: flex-start; pointer-events: none; }
|
| 457 |
-
.hud-left, .hud-right { display: flex; flex-direction: column; gap: 10px; }
|
| 458 |
-
.hud-button, .copy-link-button {
|
| 459 |
-
background-color: var(--surface-color); color: var(--text-color); border: 1px solid var(--primary-color);
|
| 460 |
-
border-radius: var(--border-radius); padding: 8px 12px; cursor: pointer;
|
| 461 |
-
font-size: 0.9rem; transition: all 0.2s; pointer-events: all;
|
| 462 |
}
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
}
|
| 472 |
-
#
|
| 473 |
-
#
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
#
|
| 479 |
-
|
| 480 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
}
|
| 482 |
-
#youtube-player { width: 100%; height: 100%; }
|
| 483 |
</style>
|
| 484 |
-
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
| 485 |
-
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
|
| 486 |
-
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.2.0/dist/aframe-extras.min.js"></script>
|
| 487 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script>
|
| 488 |
-
<script src="https://www.youtube.com/iframe_api"></script>
|
| 489 |
</head>
|
| 490 |
<body>
|
| 491 |
-
<div
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
<
|
|
|
|
| 495 |
</div>
|
| 496 |
-
<div
|
| 497 |
-
|
| 498 |
-
<
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
|
|
|
|
|
|
|
|
|
| 509 |
</div>
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
</div>
|
| 512 |
</div>
|
|
|
|
|
|
|
|
|
|
| 513 |
|
| 514 |
-
<div id="game-display" class="game-ui-overlay">
|
| 515 |
-
<h2></h2>
|
| 516 |
-
<p id="game-description"></p>
|
| 517 |
-
<div id="game-content"></div>
|
| 518 |
-
</div>
|
| 519 |
-
|
| 520 |
-
<div id="youtube-player-container">
|
| 521 |
-
<div id="youtube-player"></div>
|
| 522 |
-
</div>
|
| 523 |
-
|
| 524 |
-
<a-scene background="color: #ECECEC" renderer="colorManagement: true">
|
| 525 |
-
<a-assets>
|
| 526 |
-
<video id="local-video" autoplay playsinline muted style="display:none"></video>
|
| 527 |
-
</a-assets>
|
| 528 |
-
|
| 529 |
-
<a-entity id="player" position="0 1.6 0" movement-controls="speed: 0.15;" look-controls="pointerLockEnabled: true">
|
| 530 |
-
<a-camera></a-camera>
|
| 531 |
-
</a-entity>
|
| 532 |
-
|
| 533 |
-
<a-entity id="players-container"></a-entity>
|
| 534 |
-
|
| 535 |
-
<a-plane id="youtube-screen" position="0 3 -10" rotation="0 0 0" width="16" height="9" material="shader: flat; color: #111"></a-plane>
|
| 536 |
-
|
| 537 |
-
<a-sky color="#6EBAA7"></a-sky>
|
| 538 |
-
<a-plane position="0 0 -4" rotation="-90 0 0" width="50" height="50" color="#7BC8A4" shadow></a-plane>
|
| 539 |
-
|
| 540 |
-
<a-light type="ambient" color="#BBB"></a-light>
|
| 541 |
-
<a-light type="directional" position="-1 1 1" intensity="0.6"></a-light>
|
| 542 |
-
</a-scene>
|
| 543 |
-
|
| 544 |
-
<script>
|
| 545 |
const socket = io();
|
| 546 |
const token = '{{ token }}';
|
| 547 |
const username = '{{ username }}';
|
| 548 |
-
const is_guest = {{ is_guest|tojson }};
|
| 549 |
const isAdmin = {{ is_admin|tojson }};
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
let
|
| 553 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
let isHandlingSync = false;
|
| 555 |
|
| 556 |
-
function
|
| 557 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
}
|
| 559 |
|
| 560 |
-
function
|
| 561 |
-
const
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
}
|
| 571 |
});
|
| 572 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
|
| 574 |
-
|
| 575 |
-
|
| 576 |
}
|
| 577 |
|
| 578 |
-
function
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
}
|
| 588 |
-
|
| 589 |
-
function
|
| 590 |
-
|
| 591 |
-
const
|
| 592 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
}
|
| 594 |
|
| 595 |
-
function
|
| 596 |
-
if (
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
}
|
| 601 |
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
|
| 616 |
-
|
| 617 |
-
const
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
}
|
| 622 |
-
}
|
| 623 |
-
if (data.youtube_url) {
|
| 624 |
-
updateYoutubePlayer(data.youtube_url, data.youtube_state);
|
| 625 |
-
}
|
| 626 |
-
});
|
| 627 |
-
|
| 628 |
-
socket.on('player_joined', (data) => {
|
| 629 |
-
if (data.sid !== socket.id) {
|
| 630 |
-
createPlayerAvatar(data.sid, data.player_data);
|
| 631 |
-
const peerConnection = createPeerConnection(data.sid);
|
| 632 |
-
peerConnection.createOffer()
|
| 633 |
-
.then(offer => peerConnection.setLocalDescription(offer))
|
| 634 |
-
.then(() => {
|
| 635 |
-
socket.emit('signal', { to: data.sid, from_sid: socket.id, token, signal: peerConnection.localDescription });
|
| 636 |
-
});
|
| 637 |
-
}
|
| 638 |
-
});
|
| 639 |
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
playerEntity.parentNode.removeChild(playerEntity);
|
| 644 |
-
}
|
| 645 |
-
if (peers[data.sid]) {
|
| 646 |
-
peers[data.sid].close();
|
| 647 |
-
delete peers[data.sid];
|
| 648 |
-
}
|
| 649 |
-
});
|
| 650 |
-
|
| 651 |
-
socket.on('signal', data => {
|
| 652 |
-
let peerConnection = peers[data.from_sid];
|
| 653 |
-
if (!peerConnection) {
|
| 654 |
-
peerConnection = createPeerConnection(data.from_sid);
|
| 655 |
-
}
|
| 656 |
|
| 657 |
-
if (
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
.
|
| 661 |
-
|
| 662 |
-
socket.emit('signal', { to: data.from_sid, from_sid: socket.id, token, signal: peerConnection.localDescription });
|
| 663 |
-
});
|
| 664 |
-
} else if (data.signal.type === 'answer') {
|
| 665 |
-
peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal));
|
| 666 |
-
} else if (data.signal.type === 'candidate') {
|
| 667 |
-
peerConnection.addIceCandidate(new RTCIceCandidate(data.signal.candidate));
|
| 668 |
}
|
| 669 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 670 |
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
|
|
|
|
|
|
| 678 |
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
socket.emit('signal', { to: remoteSid, from_sid: socket.id, token, signal: { type: 'candidate', candidate: event.candidate } });
|
| 682 |
-
}
|
| 683 |
-
};
|
| 684 |
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
}
|
| 697 |
-
}
|
| 698 |
-
return peerConnection;
|
| 699 |
}
|
| 700 |
|
| 701 |
-
function
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
}
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
const
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
const head = playerEntity.querySelector('.player-head');
|
| 721 |
-
if (head) {
|
| 722 |
-
head.setAttribute('rotation', data.rotation);
|
| 723 |
-
}
|
| 724 |
-
}
|
| 725 |
}
|
| 726 |
-
}
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
isHandlingSync = true;
|
| 745 |
-
const videoId = getYouTubeVideoId(player.getVideoUrl());
|
| 746 |
-
const screen = document.getElementById('youtube-screen');
|
| 747 |
-
screen.setAttribute('material', `shader: flat; src: #youtube-video-asset-${videoId}`);
|
| 748 |
-
|
| 749 |
-
if (data.action === 'play') {
|
| 750 |
-
player.seekTo(data.time, true);
|
| 751 |
-
player.playVideo();
|
| 752 |
-
} else if (data.action === 'pause') {
|
| 753 |
-
player.pauseVideo();
|
| 754 |
-
player.seekTo(data.time, true);
|
| 755 |
-
}
|
| 756 |
}
|
| 757 |
-
});
|
| 758 |
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
});
|
| 762 |
|
| 763 |
-
function
|
| 764 |
-
|
| 765 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
|
| 767 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
|
|
|
|
|
|
|
|
|
| 775 |
|
| 776 |
-
|
| 777 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 778 |
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
if (
|
| 785 |
-
|
| 786 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
}
|
| 788 |
-
}
|
| 789 |
|
| 790 |
-
|
|
|
|
| 791 |
isHandlingSync = true;
|
| 792 |
-
if(
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
|
|
|
|
|
|
| 798 |
}
|
| 799 |
-
}
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
}
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
const
|
| 808 |
-
const
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
document.getElementById('game-description').innerText = gameInfo.description;
|
| 813 |
-
document.getElementById('game-content').innerHTML = '';
|
| 814 |
-
|
| 815 |
-
if (gameId === 'crocodile') initCrocodile(gameId);
|
| 816 |
-
else if (gameId === 'alias') initAlias(gameId);
|
| 817 |
-
else if (gameId === 'mafia') initMafia(gameId, gameInfo);
|
| 818 |
-
else if (gameId === 'durak') initDurak(gameId, gameInfo);
|
| 819 |
-
});
|
| 820 |
-
|
| 821 |
-
socket.on('update_game_state', (data) => {
|
| 822 |
-
const gameId = data.game_id;
|
| 823 |
-
if (gameId === 'crocodile') updateCrocodileState(data.state);
|
| 824 |
-
else if (gameId === 'alias') updateAliasState(data.state);
|
| 825 |
-
else if (gameId === 'mafia') updateMafiaState(data.state);
|
| 826 |
-
else if (gameId === 'durak') updateDurakState(data.state);
|
| 827 |
-
});
|
| 828 |
-
|
| 829 |
-
function initCrocodile(gameId) {
|
| 830 |
-
const gameContent = document.getElementById('game-content');
|
| 831 |
-
gameContent.innerHTML = `
|
| 832 |
-
<div id="crocodile-presenter-view" style="display:none;">
|
| 833 |
-
<p>Вы ведущий! Ваше слово: <b id="crocodile-word"></b></p>
|
| 834 |
-
</div>
|
| 835 |
-
<div id="crocodile-guesser-view" style="display:none;">
|
| 836 |
-
<input type="text" id="crocodile-guess-input" class="game-input" placeholder="Ваша догадка">
|
| 837 |
-
<button id="crocodile-guess-button" class="game-button">Угадать</button>
|
| 838 |
-
</div>
|
| 839 |
-
<div id="crocodile-admin-view" style="display:none;">
|
| 840 |
-
<input type="text" id="crocodile-word-input" class="game-input" placeholder="Введите слово для игры">
|
| 841 |
-
<button id="start-turn-button" class="game-button">Начать ход</button>
|
| 842 |
-
</div>
|
| 843 |
-
<div id="crocodile-timer"></div>
|
| 844 |
-
<div id="crocodile-guesses"></div>
|
| 845 |
-
`;
|
| 846 |
-
document.getElementById('start-turn-button').onclick = () => {
|
| 847 |
-
const word = document.getElementById('crocodile-word-input').value;
|
| 848 |
-
if(word) socket.emit('game_action', { token, game_id: gameId, action: 'start_turn', word });
|
| 849 |
-
};
|
| 850 |
-
document.getElementById('crocodile-guess-button').onclick = () => {
|
| 851 |
-
const guess = document.getElementById('crocodile-guess-input').value;
|
| 852 |
-
if(guess) socket.emit('game_action', { token, game_id: gameId, action: 'guess', value: guess, user: username });
|
| 853 |
-
};
|
| 854 |
}
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
if (state.isTimeUp) guessesDiv.innerHTML += `<h4>Время вышло! Слово: ${state.word}</h4>`;
|
| 866 |
}
|
| 867 |
|
| 868 |
-
function
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 873 |
}
|
| 874 |
-
function updateMafiaState(state) {}
|
| 875 |
|
| 876 |
-
function
|
| 877 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
</script>
|
| 879 |
</body>
|
| 880 |
</html>
|
| 881 |
-
''', token=token,
|
| 882 |
-
|
| 883 |
-
@
|
| 884 |
-
def
|
| 885 |
-
if token not in rooms:
|
| 886 |
-
return "Комната не найдена", 404
|
| 887 |
-
guest_id = 'Гость_' + generate_token()[:6]
|
| 888 |
-
session['guest_id'] = guest_id
|
| 889 |
-
if 'username' in session:
|
| 890 |
-
session.pop('username')
|
| 891 |
-
return redirect(url_for('room', token=token))
|
| 892 |
-
|
| 893 |
-
@app.route('/guest_login/<token>')
|
| 894 |
-
def guest_login(token):
|
| 895 |
-
if token not in rooms:
|
| 896 |
-
return "Комната не найдена", 404
|
| 897 |
-
return render_template_string('''
|
| 898 |
-
<!DOCTYPE html><html><head><title>Вход для гостей</title></head><body>
|
| 899 |
-
<h1>Вход в комнату как Гость</h1>
|
| 900 |
-
<a href="{{ url_for('join_as_guest', token=token) }}">Войти как гость</a>
|
| 901 |
-
<p>Или <a href="/">войдите в свой аккаунт</a>.</p>
|
| 902 |
-
</body></html>
|
| 903 |
-
''', token=token)
|
| 904 |
-
|
| 905 |
-
@socketio.on('join')
|
| 906 |
-
def handle_join(data):
|
| 907 |
token = data['token']
|
| 908 |
username = data['username']
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
join_room(token)
|
| 914 |
-
|
| 915 |
-
emit('init_room', {
|
| 916 |
-
'players': rooms[token]['players'],
|
| 917 |
-
'youtube_url': rooms[token].get('youtube_url'),
|
| 918 |
-
'youtube_state': rooms[token].get('youtube_state')
|
| 919 |
-
}, to=sid)
|
| 920 |
-
|
| 921 |
-
x_pos = random.uniform(-10, 10)
|
| 922 |
-
z_pos = random.uniform(-5, 5)
|
| 923 |
|
| 924 |
-
|
|
|
|
|
|
|
| 925 |
'username': username,
|
| 926 |
-
'
|
| 927 |
-
'
|
|
|
|
|
|
|
| 928 |
}
|
| 929 |
-
rooms[token]['players'][sid] = player_data
|
| 930 |
|
| 931 |
-
emit('player_joined', {'
|
| 932 |
-
|
| 933 |
|
| 934 |
@socketio.on('disconnect')
|
| 935 |
def handle_disconnect():
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
emit('player_left',
|
| 941 |
-
|
| 942 |
-
if not room_data['players']:
|
| 943 |
-
del rooms[token]
|
| 944 |
-
|
| 945 |
-
save_json(ROOMS_DB, rooms)
|
| 946 |
break
|
| 947 |
|
| 948 |
-
@socketio.on('
|
| 949 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 950 |
token = data['token']
|
| 951 |
-
if token in
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
'
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 965 |
|
| 966 |
@socketio.on('set_youtube_url')
|
| 967 |
def handle_set_youtube_url(data):
|
| 968 |
token = data['token']
|
| 969 |
url = data['url']
|
| 970 |
-
if token in rooms and rooms[token]['admin'] == session.get('username'):
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
|
|
|
|
|
|
|
|
|
| 975 |
|
| 976 |
@socketio.on('youtube_state_change')
|
| 977 |
def handle_youtube_state_change(data):
|
| 978 |
token = data['token']
|
| 979 |
-
if token in rooms:
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
|
|
|
|
|
|
| 984 |
emit('youtube_state_change', data, room=token, include_self=False)
|
| 985 |
|
| 986 |
@socketio.on('request_youtube_state')
|
| 987 |
def handle_request_youtube_state(data):
|
| 988 |
token = data['token']
|
| 989 |
-
if token in
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
game_id = data['game_id']
|
| 1002 |
-
username = session.get('username')
|
| 1003 |
-
if token in rooms and rooms[token].get('admin') == username:
|
| 1004 |
-
rooms[token]['current_game'] = game_id
|
| 1005 |
-
if token not in games_data[game_id]['state']:
|
| 1006 |
-
games_data[game_id]['state'][token] = {}
|
| 1007 |
-
|
| 1008 |
-
player_sids = list(rooms[token]['players'].keys())
|
| 1009 |
-
players = [rooms[token]['players'][sid]['username'] for sid in player_sids]
|
| 1010 |
-
games_data[game_id]['state'][token]['players'] = players
|
| 1011 |
-
|
| 1012 |
-
save_json(ROOMS_DB, rooms)
|
| 1013 |
-
save_json(GAMES_DB, games_data)
|
| 1014 |
-
emit('game_started', {'game_id': game_id}, room=token)
|
| 1015 |
-
emit('update_game_state', {'game_id': game_id, 'state': games_data[game_id]['state'][token]}, room=token)
|
| 1016 |
-
|
| 1017 |
-
@socketio.on('game_action')
|
| 1018 |
-
def handle_game_action(data):
|
| 1019 |
-
token = data['token']
|
| 1020 |
-
game_id = data.get('game_id')
|
| 1021 |
-
action = data.get('action')
|
| 1022 |
-
user = data.get('user')
|
| 1023 |
-
|
| 1024 |
-
if not (token in rooms and game_id and rooms[token].get('current_game') == game_id):
|
| 1025 |
-
return
|
| 1026 |
-
|
| 1027 |
-
state = games_data[game_id]['state'][token]
|
| 1028 |
-
|
| 1029 |
-
if game_id in ['crocodile', 'alias']:
|
| 1030 |
-
if action == 'start_turn':
|
| 1031 |
-
players = state.get('players', [])
|
| 1032 |
-
if not players: return
|
| 1033 |
-
state['word'] = data.get('word')
|
| 1034 |
-
state['presenter'] = random.choice(players)
|
| 1035 |
-
state['guesses'] = []
|
| 1036 |
-
state['timer'] = 60
|
| 1037 |
-
state['isRunning'] = True
|
| 1038 |
-
state['winner'] = None
|
| 1039 |
-
state['isTimeUp'] = False
|
| 1040 |
-
socketio.start_background_task(target=game_timer, token=token, game_id=game_id)
|
| 1041 |
-
elif action == 'guess' and state.get('isRunning'):
|
| 1042 |
-
value = data.get('value', '').lower()
|
| 1043 |
-
word = state.get('word', '').lower()
|
| 1044 |
-
result = "Угадано!" if value == word else "Неверно"
|
| 1045 |
-
state['guesses'].append({'user': user, 'value': data.get('value'), 'result': result})
|
| 1046 |
-
if result == "Угадано!":
|
| 1047 |
-
state['isRunning'] = False
|
| 1048 |
-
state['winner'] = user
|
| 1049 |
-
|
| 1050 |
-
games_data[game_id]['state'][token] = state
|
| 1051 |
-
save_json(GAMES_DB, games_data)
|
| 1052 |
-
emit('update_game_state', {'game_id': game_id, 'state': state}, room=token)
|
| 1053 |
-
|
| 1054 |
-
def game_timer(token, game_id):
|
| 1055 |
-
with app.app_context():
|
| 1056 |
-
while True:
|
| 1057 |
-
socketio.sleep(1)
|
| 1058 |
-
state = games_data[game_id]['state'].get(token)
|
| 1059 |
-
if not state or not state.get('isRunning'):
|
| 1060 |
-
break
|
| 1061 |
-
|
| 1062 |
-
state['timer'] -= 1
|
| 1063 |
-
if state['timer'] <= 0:
|
| 1064 |
-
state['isRunning'] = False
|
| 1065 |
-
state['isTimeUp'] = True
|
| 1066 |
-
|
| 1067 |
-
games_data[game_id]['state'][token] = state
|
| 1068 |
-
save_json(GAMES_DB, games_data)
|
| 1069 |
-
socketio.emit('update_game_state', {'game_id': game_id, 'state': state}, room=token)
|
| 1070 |
|
| 1071 |
if __name__ == '__main__':
|
| 1072 |
-
if
|
| 1073 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 1074 |
backup_thread.start()
|
| 1075 |
socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True)
|
|
|
|
| 13 |
from urllib.parse import urlparse, parse_qs
|
| 14 |
|
| 15 |
app = Flask(__name__)
|
| 16 |
+
app.config['SECRET_KEY'] = 'your-very-secret-key-here-for-metaverse'
|
| 17 |
socketio = SocketIO(app, async_mode='threading')
|
| 18 |
|
| 19 |
REPO_ID = "flpolprojects/Clients"
|
| 20 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
|
|
|
| 21 |
|
| 22 |
+
DB_FILE = os.path.join(app.root_path, 'metaverse_db.json')
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
def load_db():
|
| 25 |
try:
|
| 26 |
+
if HF_TOKEN:
|
| 27 |
+
local_path = hf_hub_download(
|
| 28 |
+
repo_id=REPO_ID,
|
| 29 |
+
filename="metaverse_db.json",
|
| 30 |
+
repo_type="dataset",
|
| 31 |
+
token=HF_TOKEN,
|
| 32 |
+
local_dir=".",
|
| 33 |
+
local_dir_use_symlinks=False
|
| 34 |
+
)
|
| 35 |
+
print(f"База данных успешно скачана с Hugging Face Hub: {local_path}")
|
| 36 |
+
except RepositoryNotFoundError:
|
| 37 |
+
print("Репозиторий или файл не найден. Используется/создается локальная версия.")
|
| 38 |
except Exception as e:
|
| 39 |
+
print(f"Ошибка при скачивании с Hugging Face Hub: {e}. Используется/создается локальная версия.")
|
|
|
|
| 40 |
|
|
|
|
| 41 |
try:
|
| 42 |
+
with open(DB_FILE, 'r', encoding='utf-8') as f:
|
| 43 |
+
return json.load(f)
|
| 44 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
| 45 |
+
return {'users': {}, 'rooms': {}}
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
+
def save_db(data):
|
| 48 |
try:
|
| 49 |
+
with open(DB_FILE, 'w', encoding='utf-8') as f:
|
| 50 |
+
json.dump(data, f, indent=4, ensure_ascii=False)
|
| 51 |
+
except OSError as e:
|
| 52 |
+
print(f"Ошибка сохранения JSON в {DB_FILE}: {e}")
|
| 53 |
+
|
| 54 |
+
if HF_TOKEN:
|
| 55 |
+
try:
|
| 56 |
+
api = HfApi()
|
| 57 |
+
api.upload_file(
|
| 58 |
+
path_or_fileobj=DB_FILE,
|
| 59 |
+
path_in_repo="metaverse_db.json",
|
| 60 |
+
repo_id=REPO_ID,
|
| 61 |
+
repo_type="dataset",
|
| 62 |
+
token=HF_TOKEN,
|
| 63 |
+
commit_message=f"Backup: metaverse_db.json ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})"
|
| 64 |
+
)
|
| 65 |
+
print(f"Файл {DB_FILE} успешно загружен на Hugging Face Hub.")
|
| 66 |
+
except Exception as e:
|
| 67 |
+
print(f"Ошибка при загрузке файла на Hugging Face Hub: {e}")
|
| 68 |
+
|
| 69 |
+
db = load_db()
|
| 70 |
+
if 'users' not in db: db['users'] = {}
|
| 71 |
+
if 'rooms' not in db: db['rooms'] = {}
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
def periodic_backup():
|
| 75 |
while True:
|
| 76 |
+
time.sleep(300)
|
| 77 |
+
print("Выполняется периодическое сохранение...")
|
| 78 |
+
save_db(db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
def generate_token():
|
| 81 |
+
return ''.join(random.choices(string.ascii_letters + string.digits, k=16))
|
| 82 |
|
| 83 |
def hash_password(password):
|
| 84 |
return hashlib.sha256(password.encode('utf-8')).hexdigest()
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
@app.route('/', methods=['GET', 'POST'])
|
| 87 |
def index():
|
| 88 |
if 'username' in session:
|
|
|
|
| 94 |
password = request.form.get('password')
|
| 95 |
|
| 96 |
if action == 'register':
|
| 97 |
+
if username in db['users']:
|
| 98 |
return "Пользователь уже существует", 400
|
| 99 |
+
db['users'][username] = {'password': hash_password(password)}
|
| 100 |
+
save_db(db)
|
| 101 |
session['username'] = username
|
| 102 |
return redirect(url_for('dashboard'))
|
| 103 |
|
| 104 |
elif action == 'login':
|
| 105 |
+
user = db['users'].get(username)
|
| 106 |
+
if user and user['password'] == hash_password(password):
|
| 107 |
session['username'] = username
|
| 108 |
return redirect(url_for('dashboard'))
|
| 109 |
return "Неверный логин или пароль", 401
|
|
|
|
| 114 |
<head>
|
| 115 |
<meta charset="UTF-8">
|
| 116 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 117 |
+
<title>Metaverse Login</title>
|
| 118 |
<style>
|
| 119 |
:root {
|
| 120 |
+
--primary-color: #007bff;
|
| 121 |
+
--secondary-color: #0056b3;
|
| 122 |
+
--background-color: #1a1a1d;
|
| 123 |
+
--surface-color: #2c2c34;
|
| 124 |
+
--text-color: #f0f0f0;
|
|
|
|
| 125 |
--font-family: 'Roboto', sans-serif;
|
| 126 |
--border-radius: 12px;
|
|
|
|
| 127 |
}
|
| 128 |
body {
|
| 129 |
font-family: var(--font-family);
|
| 130 |
background-color: var(--background-color);
|
| 131 |
color: var(--text-color);
|
| 132 |
margin: 0;
|
|
|
|
| 133 |
display: flex;
|
| 134 |
justify-content: center;
|
| 135 |
align-items: center;
|
|
|
|
| 139 |
background-color: var(--surface-color);
|
| 140 |
padding: 2rem;
|
| 141 |
border-radius: var(--border-radius);
|
| 142 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.4);
|
| 143 |
width: 90%;
|
| 144 |
max-width: 400px;
|
| 145 |
text-align: center;
|
| 146 |
}
|
| 147 |
h1 {
|
| 148 |
+
font-size: 2.5rem;
|
| 149 |
margin-bottom: 1.5rem;
|
| 150 |
color: var(--primary-color);
|
| 151 |
+
text-shadow: 0 0 10px var(--primary-color);
|
| 152 |
}
|
| 153 |
input, button {
|
| 154 |
display: block;
|
| 155 |
width: 100%;
|
| 156 |
+
padding: 0.85rem;
|
| 157 |
+
margin-bottom: 1.2rem;
|
| 158 |
+
border: 1px solid #444;
|
| 159 |
+
background-color: #333;
|
| 160 |
+
color: var(--text-color);
|
| 161 |
border-radius: var(--border-radius);
|
| 162 |
font-size: 1rem;
|
| 163 |
box-sizing: border-box;
|
| 164 |
+
transition: all 0.3s ease;
|
| 165 |
}
|
| 166 |
input:focus {
|
| 167 |
outline: none;
|
| 168 |
border-color: var(--primary-color);
|
| 169 |
+
box-shadow: 0 0 8px var(--primary-color);
|
| 170 |
}
|
| 171 |
button {
|
| 172 |
background-color: var(--primary-color);
|
|
|
|
| 173 |
cursor: pointer;
|
| 174 |
border: none;
|
| 175 |
font-weight: 500;
|
|
|
|
| 176 |
}
|
| 177 |
button:hover {
|
| 178 |
background-color: var(--secondary-color);
|
| 179 |
+
transform: translateY(-2px);
|
| 180 |
+
box-shadow: 0 2px 8px rgba(0,123,255,0.5);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
}
|
| 182 |
</style>
|
| 183 |
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
| 184 |
</head>
|
| 185 |
<body>
|
| 186 |
<div class="container">
|
| 187 |
+
<h1>VOXELVERSE</h1>
|
| 188 |
<form method="post">
|
| 189 |
<input type="text" name="username" placeholder="Логин" required>
|
| 190 |
<input type="password" name="password" placeholder="Пароль" required>
|
| 191 |
<button type="submit" name="action" value="login">Войти</button>
|
| 192 |
+
<button type="submit" name="action" value="register">Регистрация</button>
|
| 193 |
</form>
|
| 194 |
</div>
|
| 195 |
</body>
|
| 196 |
</html>
|
| 197 |
''')
|
| 198 |
|
|
|
|
| 199 |
@app.route('/dashboard', methods=['GET', 'POST'])
|
| 200 |
def dashboard():
|
| 201 |
if 'username' not in session:
|
| 202 |
return redirect(url_for('index'))
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
if request.method == 'POST':
|
| 205 |
action = request.form.get('action')
|
| 206 |
if action == 'create':
|
| 207 |
token = generate_token()
|
| 208 |
+
db['rooms'][token] = {
|
|
|
|
| 209 |
'admin': session['username'],
|
| 210 |
+
'players': {},
|
| 211 |
+
'youtube': {
|
| 212 |
+
'url': '',
|
| 213 |
+
'state': {'isPlaying': False, 'currentTime': 0, 'last_sync_time': time.time()}
|
| 214 |
+
},
|
| 215 |
+
'chat': []
|
| 216 |
}
|
| 217 |
+
save_db(db)
|
| 218 |
+
return redirect(url_for('world', token=token))
|
|
|
|
|
|
|
| 219 |
elif action == 'join':
|
| 220 |
token = request.form.get('token')
|
| 221 |
+
if token in db['rooms']:
|
| 222 |
+
return redirect(url_for('world', token=token))
|
| 223 |
+
return "Мир не найден", 404
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
return render_template_string('''
|
| 226 |
<!DOCTYPE html>
|
|
|
|
| 231 |
<title>Панель управления</title>
|
| 232 |
<style>
|
| 233 |
:root {
|
| 234 |
+
--primary-color: #007bff;
|
| 235 |
+
--secondary-color: #0056b3;
|
| 236 |
+
--background-color: #1a1a1d;
|
| 237 |
+
--surface-color: #2c2c34;
|
| 238 |
+
--text-color: #f0f0f0;
|
| 239 |
+
--accent-color: #dc3545;
|
| 240 |
--font-family: 'Roboto', sans-serif;
|
| 241 |
--border-radius: 12px;
|
|
|
|
| 242 |
}
|
| 243 |
body {
|
| 244 |
font-family: var(--font-family);
|
| 245 |
background-color: var(--background-color);
|
| 246 |
color: var(--text-color);
|
| 247 |
margin: 0;
|
| 248 |
+
padding: 2rem;
|
| 249 |
display: flex;
|
| 250 |
flex-direction: column;
|
| 251 |
align-items: center;
|
|
|
|
| 255 |
background-color: var(--surface-color);
|
| 256 |
padding: 2rem;
|
| 257 |
border-radius: var(--border-radius);
|
| 258 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.4);
|
| 259 |
width: 90%;
|
| 260 |
+
max-width: 500px;
|
| 261 |
text-align: center;
|
|
|
|
| 262 |
}
|
| 263 |
h1 {
|
| 264 |
font-size: 2rem;
|
|
|
|
| 268 |
input, button {
|
| 269 |
display: block;
|
| 270 |
width: 100%;
|
| 271 |
+
padding: 0.85rem;
|
| 272 |
margin-bottom: 1rem;
|
| 273 |
+
border: 1px solid #444;
|
| 274 |
+
background-color: #333;
|
| 275 |
+
color: var(--text-color);
|
| 276 |
border-radius: var(--border-radius);
|
| 277 |
font-size: 1rem;
|
| 278 |
box-sizing: border-box;
|
| 279 |
+
transition: all 0.3s ease;
|
| 280 |
}
|
| 281 |
input:focus {
|
| 282 |
outline: none;
|
| 283 |
border-color: var(--primary-color);
|
| 284 |
+
box-shadow: 0 0 8px var(--primary-color);
|
| 285 |
}
|
| 286 |
button {
|
| 287 |
background-color: var(--primary-color);
|
|
|
|
| 288 |
cursor: pointer;
|
| 289 |
border: none;
|
| 290 |
font-weight: 500;
|
|
|
|
| 291 |
}
|
| 292 |
button:hover {
|
| 293 |
background-color: var(--secondary-color);
|
| 294 |
+
transform: translateY(-2px);
|
|
|
|
|
|
|
| 295 |
}
|
| 296 |
.logout-button {
|
| 297 |
background-color: var(--accent-color);
|
|
|
|
|
|
|
| 298 |
}
|
| 299 |
.logout-button:hover {
|
| 300 |
+
background-color: #b22a37;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
}
|
| 302 |
</style>
|
| 303 |
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
|
|
|
| 306 |
<div class="container">
|
| 307 |
<h1>Добро пожаловать, {{ session['username'] }}</h1>
|
| 308 |
<form method="post">
|
| 309 |
+
<button type="submit" name="action" value="create">Создать новый мир</button>
|
| 310 |
</form>
|
| 311 |
+
<hr style="border-color: #444; margin: 1.5rem 0;">
|
| 312 |
<form method="post">
|
| 313 |
+
<input type="text" name="token" placeholder="Введите ID мира" required>
|
| 314 |
+
<button type="submit" name="action" value="join">Войти в мир</button>
|
| 315 |
</form>
|
| 316 |
+
<form action="/logout" method="post" style="margin-top: 2rem;">
|
| 317 |
<button class="logout-button" type="submit">Выйти</button>
|
| 318 |
</form>
|
| 319 |
</div>
|
| 320 |
</body>
|
| 321 |
</html>
|
| 322 |
+
''')
|
| 323 |
|
| 324 |
@app.route('/logout', methods=['POST'])
|
| 325 |
def logout():
|
| 326 |
session.pop('username', None)
|
| 327 |
return redirect(url_for('index'))
|
| 328 |
|
| 329 |
+
@app.route('/world/<token>')
|
| 330 |
+
def world(token):
|
| 331 |
+
if 'username' not in session:
|
| 332 |
+
return redirect(url_for('index'))
|
| 333 |
+
if token not in db['rooms']:
|
|
|
|
|
|
|
| 334 |
return redirect(url_for('dashboard'))
|
| 335 |
|
| 336 |
+
username = session['username']
|
| 337 |
+
is_admin = db['rooms'][token]['admin'] == username
|
| 338 |
|
| 339 |
return render_template_string('''
|
| 340 |
<!DOCTYPE html>
|
| 341 |
<html lang="ru">
|
| 342 |
<head>
|
| 343 |
<meta charset="UTF-8">
|
| 344 |
+
<title>Voxelverse - {{ token }}</title>
|
| 345 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 346 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
| 347 |
+
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
|
| 348 |
+
<script type="importmap">
|
| 349 |
+
{
|
| 350 |
+
"imports": {
|
| 351 |
+
"three": "https://unpkg.com/three@0.157.0/build/three.module.js",
|
| 352 |
+
"three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
}
|
| 354 |
+
}
|
| 355 |
+
</script>
|
| 356 |
+
<script src="https://www.youtube.com/iframe_api"></script>
|
| 357 |
+
<style>
|
| 358 |
+
body { margin: 0; overflow: hidden; background: #000; color: #fff; font-family: monospace; }
|
| 359 |
+
canvas { display: block; }
|
| 360 |
+
#crosshair { position: fixed; top: 50%; left: 50%; width: 4px; height: 4px; background: rgba(255,255,255,0.7); border-radius: 50%; transform: translate(-50%, -50%); }
|
| 361 |
+
#ui-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
|
| 362 |
+
#chat-container { position: absolute; bottom: 10px; left: 10px; width: 350px; height: 200px; background: rgba(0,0,0,0.5); border-radius: 8px; display: flex; flex-direction: column; pointer-events: all; }
|
| 363 |
+
#chat-messages { flex-grow: 1; overflow-y: auto; padding: 10px; font-size: 14px; }
|
| 364 |
+
#chat-messages p { margin: 0 0 5px 0; }
|
| 365 |
+
#chat-input { width: 100%; border: none; background: rgba(255,255,255,0.2); color: #fff; padding: 10px; box-sizing: border-box; }
|
| 366 |
+
#joystick-container { position: fixed; bottom: 20px; left: 20px; width: 150px; height: 150px; display: none; pointer-events: all; }
|
| 367 |
+
#joystick-base { position: absolute; width: 100%; height: 100%; background: rgba(255,255,255,0.2); border-radius: 50%; }
|
| 368 |
+
#joystick-thumb { position: absolute; top: 50%; left: 50%; width: 60px; height: 60px; background: rgba(255,255,255,0.4); border-radius: 50%; transform: translate(-50%, -50%); }
|
| 369 |
+
#look-joystick-container { position: fixed; bottom: 20px; right: 20px; width: 150px; height: 150px; display: none; pointer-events: all; }
|
| 370 |
+
#youtube-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); display: none; justify-content: center; align-items: center; z-index: 100; pointer-events: all; flex-direction: column; }
|
| 371 |
+
#youtube-player-container { width: 80%; max-width: 960px; aspect-ratio: 16/9; }
|
| 372 |
+
#youtube-controls { margin-top: 20px; display: flex; gap: 10px; }
|
| 373 |
+
.yt-button { padding: 10px 20px; background: #c00; border: none; color: #fff; cursor: pointer; border-radius: 5px; }
|
| 374 |
+
#yt-url-input { padding: 10px; border-radius: 5px; border: 1px solid #555; background: #333; color: #fff; flex-grow: 1; }
|
| 375 |
+
#info-popup { position: fixed; top: 10%; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; border: 1px solid #007bff; display: none; pointer-events: all; text-align: center; }
|
| 376 |
+
@media (hover: none) and (pointer: coarse) {
|
| 377 |
+
#joystick-container, #look-joystick-container { display: block; }
|
| 378 |
+
#crosshair { display: none; }
|
| 379 |
}
|
|
|
|
| 380 |
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
</head>
|
| 382 |
<body>
|
| 383 |
+
<div id="crosshair"></div>
|
| 384 |
+
<div id="ui-container">
|
| 385 |
+
<div id="chat-container">
|
| 386 |
+
<div id="chat-messages"></div>
|
| 387 |
+
<input type="text" id="chat-input" placeholder="Нажмите Enter для чата...">
|
| 388 |
</div>
|
| 389 |
+
<div id="joystick-container">
|
| 390 |
+
<div id="joystick-base"></div>
|
| 391 |
+
<div id="joystick-thumb"></div>
|
| 392 |
+
</div>
|
| 393 |
+
<div id="look-joystick-container">
|
| 394 |
+
<div id="joystick-base"></div>
|
| 395 |
+
<div id="joystick-thumb"></div>
|
| 396 |
+
</div>
|
| 397 |
+
<div id="youtube-overlay">
|
| 398 |
+
<div id="youtube-player-container"></div>
|
| 399 |
+
<div id="youtube-controls">
|
| 400 |
+
{% if is_admin %}
|
| 401 |
+
<input type="text" id="yt-url-input" placeholder="Вставьте URL видео YouTube">
|
| 402 |
+
<button class="yt-button" id="yt-load-btn">Загрузить</button>
|
| 403 |
+
{% endif %}
|
| 404 |
+
<button class="yt-button" id="yt-close-btn">Закрыть</button>
|
| 405 |
</div>
|
| 406 |
+
</div>
|
| 407 |
+
<div id="info-popup">
|
| 408 |
+
<h2>Информация</h2>
|
| 409 |
+
<p id="info-popup-text"></p>
|
| 410 |
+
<button id="info-popup-close">Закрыть</button>
|
| 411 |
</div>
|
| 412 |
</div>
|
| 413 |
+
<script type="module">
|
| 414 |
+
import * as THREE from 'three';
|
| 415 |
+
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
const socket = io();
|
| 418 |
const token = '{{ token }}';
|
| 419 |
const username = '{{ username }}';
|
|
|
|
| 420 |
const isAdmin = {{ is_admin|tojson }};
|
| 421 |
+
|
| 422 |
+
let scene, camera, renderer, controls;
|
| 423 |
+
let player, playerVelocity, playerOnFloor;
|
| 424 |
+
const remotePlayers = {};
|
| 425 |
+
const clock = new THREE.Clock();
|
| 426 |
+
const keys = {};
|
| 427 |
+
let interactiveObjects = [];
|
| 428 |
+
|
| 429 |
+
const MAP_SIZE = 32;
|
| 430 |
+
const TILE_SIZE = 5;
|
| 431 |
+
const worldMap = [
|
| 432 |
+
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
| 433 |
+
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
| 434 |
+
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
| 435 |
+
[1,0,0,4,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
| 436 |
+
[1,0,0,4,1,1,1,1,1,1,1,4,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1],
|
| 437 |
+
[1,0,0,4,1,0,0,0,0,0,1,4,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],
|
| 438 |
+
[1,0,0,4,1,0,0,0,0,0,1,4,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],
|
| 439 |
+
[1,0,0,4,1,0,0,5,0,0,1,4,0,0,0,1,0,0,0,1,1,1,1,1,1,0,0,1,0,0,0,1],
|
| 440 |
+
[1,0,0,4,1,0,0,0,0,0,1,4,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,1],
|
| 441 |
+
[1,0,0,4,1,0,0,0,0,0,1,4,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,1,0,0,0,1],
|
| 442 |
+
[1,0,0,4,1,1,1,1,1,1,1,4,0,0,0,1,0,0,0,1,1,1,1,1,1,0,0,1,0,0,0,1],
|
| 443 |
+
[1,0,0,4,4,4,4,4,4,4,4,4,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],
|
| 444 |
+
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1],
|
| 445 |
+
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
| 446 |
+
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
| 447 |
+
[1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,2,2,2,1,1,1,1,1,1],
|
| 448 |
+
[1,2,2,2,2,2,2,2,2,1,0,0,1,2,2,2,2,2,2,2,2,2,1,2,2,2,1,2,2,2,2,1],
|
| 449 |
+
[1,2,2,2,2,2,2,2,2,1,0,0,1,2,2,2,2,2,2,2,2,2,1,2,2,2,1,2,2,2,2,1],
|
| 450 |
+
[1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,2,2,2,1,1,1,1,1,1],
|
| 451 |
+
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
| 452 |
+
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
| 453 |
+
[1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1],
|
| 454 |
+
[1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],
|
| 455 |
+
[1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],
|
| 456 |
+
[1,0,0,3,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],
|
| 457 |
+
[1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],
|
| 458 |
+
[1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1],
|
| 459 |
+
[1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1],
|
| 460 |
+
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
| 461 |
+
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
| 462 |
+
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
| 463 |
+
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
|
| 464 |
+
];
|
| 465 |
+
|
| 466 |
+
const hotspots = [
|
| 467 |
+
{ id: 'yt', type: 'youtube', pos: {x: 10 * TILE_SIZE, y: TILE_SIZE * 2, z: 7 * TILE_SIZE}, size: {x: TILE_SIZE*5, y: TILE_SIZE*3, z: 0.2}, rotation: {x: 0, y: Math.PI / 2, z:0}},
|
| 468 |
+
{ id: 'info1', type: 'info', pos: {x: 24 * TILE_SIZE, y: TILE_SIZE * 1.5, z: 3 * TILE_SIZE}, text: 'Добро пожаловать в Voxelverse!'},
|
| 469 |
+
{ id: 'tp1', type: 'teleport', pos: {x: 2 * TILE_SIZE, y: TILE_SIZE * 0.5, z: 2 * TILE_SIZE}, target: {x: 29 * TILE_SIZE, y: TILE_SIZE, z: 29 * TILE_SIZE}}
|
| 470 |
+
];
|
| 471 |
+
|
| 472 |
+
let ytPlayer;
|
| 473 |
+
let isYtPlayerReady = false;
|
| 474 |
let isHandlingSync = false;
|
| 475 |
|
| 476 |
+
function init() {
|
| 477 |
+
scene = new THREE.Scene();
|
| 478 |
+
scene.background = new THREE.Color(0x87ceeb);
|
| 479 |
+
scene.fog = new THREE.Fog(0x87ceeb, 0, TILE_SIZE * MAP_SIZE / 2);
|
| 480 |
+
|
| 481 |
+
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
| 482 |
+
|
| 483 |
+
renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 484 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 485 |
+
renderer.setPixelRatio(window.devicePixelRatio);
|
| 486 |
+
renderer.shadowMap.enabled = true;
|
| 487 |
+
document.body.appendChild(renderer.domElement);
|
| 488 |
+
|
| 489 |
+
const light = new THREE.HemisphereLight(0xeeeeff, 0x777788, 0.9);
|
| 490 |
+
light.position.set(0.5, 1, 0.75);
|
| 491 |
+
scene.add(light);
|
| 492 |
+
|
| 493 |
+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
|
| 494 |
+
dirLight.position.set(50, 200, 100);
|
| 495 |
+
dirLight.castShadow = true;
|
| 496 |
+
dirLight.shadow.camera.top = 180;
|
| 497 |
+
dirLight.shadow.camera.bottom = -100;
|
| 498 |
+
dirLight.shadow.camera.left = -120;
|
| 499 |
+
dirLight.shadow.camera.right = 120;
|
| 500 |
+
scene.add(dirLight);
|
| 501 |
+
|
| 502 |
+
createMap();
|
| 503 |
+
createPlayer();
|
| 504 |
+
setupControls();
|
| 505 |
+
setupSocketListeners();
|
| 506 |
+
setupUI();
|
| 507 |
+
|
| 508 |
+
animate();
|
| 509 |
}
|
| 510 |
|
| 511 |
+
function createMap() {
|
| 512 |
+
const textures = {
|
| 513 |
+
grass: new THREE.MeshLambertMaterial({ color: 0x559020 }),
|
| 514 |
+
stone: new THREE.MeshLambertMaterial({ color: 0x888888 }),
|
| 515 |
+
water: new THREE.MeshLambertMaterial({ color: 0x3388cc, transparent: true, opacity: 0.7 }),
|
| 516 |
+
ytScreen: new THREE.MeshBasicMaterial({ color: 0x101010 }),
|
| 517 |
+
info: new THREE.MeshBasicMaterial({ color: 0xffff00 }),
|
| 518 |
+
teleport: new THREE.MeshBasicMaterial({ color: 0x9400D3 })
|
| 519 |
+
};
|
| 520 |
+
|
| 521 |
+
const boxGeo = new THREE.BoxGeometry(TILE_SIZE, TILE_SIZE, TILE_SIZE);
|
| 522 |
+
|
| 523 |
+
for (let z = 0; z < MAP_SIZE; z++) {
|
| 524 |
+
for (let x = 0; x < MAP_SIZE; x++) {
|
| 525 |
+
const tileType = worldMap[z][x];
|
| 526 |
+
if (tileType === 2) continue;
|
| 527 |
+
let material = textures.stone;
|
| 528 |
+
if (tileType === 0) material = textures.grass;
|
| 529 |
+
|
| 530 |
+
const mesh = new THREE.Mesh(boxGeo, material);
|
| 531 |
+
mesh.position.set(x * TILE_SIZE, -TILE_SIZE / 2, z * TILE_SIZE);
|
| 532 |
+
mesh.receiveShadow = true;
|
| 533 |
+
scene.add(mesh);
|
| 534 |
+
}
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
const waterGeo = new THREE.PlaneGeometry(MAP_SIZE * TILE_SIZE, MAP_SIZE * TILE_SIZE);
|
| 538 |
+
const water = new THREE.Mesh(waterGeo, textures.water);
|
| 539 |
+
water.rotation.x = -Math.PI / 2;
|
| 540 |
+
water.position.y = -TILE_SIZE / 4;
|
| 541 |
+
scene.add(water);
|
| 542 |
+
|
| 543 |
+
hotspots.forEach(hotspot => {
|
| 544 |
+
let object;
|
| 545 |
+
if(hotspot.type === 'youtube') {
|
| 546 |
+
const screenGeo = new THREE.BoxGeometry(hotspot.size.x, hotspot.size.y, hotspot.size.z);
|
| 547 |
+
object = new THREE.Mesh(screenGeo, textures.ytScreen);
|
| 548 |
+
object.rotation.set(hotspot.rotation.x, hotspot.rotation.y, hotspot.rotation.z);
|
| 549 |
+
} else if (hotspot.type === 'info') {
|
| 550 |
+
const infoGeo = new THREE.BoxGeometry(TILE_SIZE/2, TILE_SIZE/2, TILE_SIZE/2);
|
| 551 |
+
object = new THREE.Mesh(infoGeo, textures.info);
|
| 552 |
+
} else if (hotspot.type === 'teleport') {
|
| 553 |
+
const tpGeo = new THREE.CylinderGeometry(TILE_SIZE, TILE_SIZE, 0.5, 32);
|
| 554 |
+
object = new THREE.Mesh(tpGeo, textures.teleport);
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
if (object) {
|
| 558 |
+
object.position.set(hotspot.pos.x, hotspot.pos.y, hotspot.pos.z);
|
| 559 |
+
object.userData = { type: 'hotspot', id: hotspot.id, details: hotspot };
|
| 560 |
+
scene.add(object);
|
| 561 |
+
interactiveObjects.push(object);
|
| 562 |
}
|
| 563 |
});
|
| 564 |
}
|
| 565 |
+
|
| 566 |
+
function createPlayer() {
|
| 567 |
+
const headGeo = new THREE.BoxGeometry(0.8, 0.8, 0.8);
|
| 568 |
+
const headMat = new THREE.MeshLambertMaterial({ color: 0xffdbac });
|
| 569 |
+
const head = new THREE.Mesh(headGeo, headMat);
|
| 570 |
+
head.position.y = 1.3;
|
| 571 |
+
|
| 572 |
+
const bodyGeo = new THREE.BoxGeometry(1, 1.5, 0.5);
|
| 573 |
+
const bodyMat = new THREE.MeshLambertMaterial({ color: 0x335599 });
|
| 574 |
+
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
| 575 |
+
body.position.y = 0.25;
|
| 576 |
+
|
| 577 |
+
player = new THREE.Group();
|
| 578 |
+
player.add(head);
|
| 579 |
+
player.add(body);
|
| 580 |
+
|
| 581 |
+
player.position.set(5 * TILE_SIZE, TILE_SIZE, 5 * TILE_SIZE);
|
| 582 |
+
player.add(camera);
|
| 583 |
+
camera.position.set(0, 1.6, 0);
|
| 584 |
+
scene.add(player);
|
| 585 |
|
| 586 |
+
playerVelocity = new THREE.Vector3();
|
| 587 |
+
playerOnFloor = false;
|
| 588 |
}
|
| 589 |
|
| 590 |
+
function addRemotePlayer(id, data) {
|
| 591 |
+
const headGeo = new THREE.BoxGeometry(0.8, 0.8, 0.8);
|
| 592 |
+
const headMat = new THREE.MeshLambertMaterial({ color: data.color || 0xffdbac });
|
| 593 |
+
const head = new THREE.Mesh(headGeo, headMat);
|
| 594 |
+
head.position.y = 1.3;
|
| 595 |
+
|
| 596 |
+
const bodyGeo = new THREE.BoxGeometry(1, 1.5, 0.5);
|
| 597 |
+
const bodyMat = new THREE.MeshLambertMaterial({ color: data.bodyColor || 0x993355 });
|
| 598 |
+
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
| 599 |
+
body.position.y = 0.25;
|
| 600 |
+
|
| 601 |
+
const remotePlayer = new THREE.Group();
|
| 602 |
+
remotePlayer.add(head);
|
| 603 |
+
remotePlayer.add(body);
|
| 604 |
+
|
| 605 |
+
const nameText = createNameLabel(data.username);
|
| 606 |
+
nameText.position.y = 2.5;
|
| 607 |
+
remotePlayer.add(nameText);
|
| 608 |
+
|
| 609 |
+
remotePlayer.position.set(data.pos.x, data.pos.y, data.pos.z);
|
| 610 |
+
remotePlayer.rotation.set(data.rot.x, data.rot.y, data.rot.z);
|
| 611 |
+
|
| 612 |
+
remotePlayers[id] = { object: remotePlayer, targetPos: new THREE.Vector3(), targetRot: new THREE.Euler() };
|
| 613 |
+
remotePlayers[id].targetPos.copy(remotePlayer.position);
|
| 614 |
+
remotePlayers[id].targetRot.copy(remotePlayer.rotation);
|
| 615 |
+
|
| 616 |
+
scene.add(remotePlayer);
|
| 617 |
}
|
| 618 |
+
|
| 619 |
+
function createNameLabel(name) {
|
| 620 |
+
const canvas = document.createElement('canvas');
|
| 621 |
+
const context = canvas.getContext('2d');
|
| 622 |
+
context.font = 'Bold 40px Arial';
|
| 623 |
+
const textWidth = context.measureText(name).width;
|
| 624 |
+
canvas.width = textWidth;
|
| 625 |
+
canvas.height = 50;
|
| 626 |
+
context.font = 'Bold 40px Arial';
|
| 627 |
+
context.fillStyle = 'rgba(255, 255, 255, 0.95)';
|
| 628 |
+
context.fillText(name, 0, 40);
|
| 629 |
+
const texture = new THREE.CanvasTexture(canvas);
|
| 630 |
+
const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
|
| 631 |
+
const sprite = new THREE.Sprite(spriteMaterial);
|
| 632 |
+
sprite.scale.set(textWidth / 100, 0.5, 1.0);
|
| 633 |
+
return sprite;
|
| 634 |
}
|
| 635 |
|
| 636 |
+
function removeRemotePlayer(id) {
|
| 637 |
+
if (remotePlayers[id]) {
|
| 638 |
+
scene.remove(remotePlayers[id].object);
|
| 639 |
+
delete remotePlayers[id];
|
| 640 |
+
}
|
| 641 |
}
|
| 642 |
|
| 643 |
+
function setupControls() {
|
| 644 |
+
controls = new PointerLockControls(camera, renderer.domElement);
|
| 645 |
+
const blocker = document.body;
|
| 646 |
+
blocker.addEventListener('click', () => controls.lock());
|
| 647 |
+
|
| 648 |
+
document.addEventListener('keydown', (e) => keys[e.code] = true);
|
| 649 |
+
document.addEventListener('keyup', (e) => keys[e.code] = false);
|
| 650 |
+
|
| 651 |
+
const joystick = document.getElementById('joystick-container');
|
| 652 |
+
const joystickThumb = document.getElementById('joystick-thumb');
|
| 653 |
+
let joystickActive = false;
|
| 654 |
+
let joystickStart = { x: 0, y: 0 };
|
| 655 |
+
let joystickMove = { x: 0, y: 0 };
|
| 656 |
+
|
| 657 |
+
joystick.addEventListener('touchstart', (e) => {
|
| 658 |
+
e.preventDefault();
|
| 659 |
+
joystickActive = true;
|
| 660 |
+
joystickStart.x = e.touches[0].clientX;
|
| 661 |
+
joystickStart.y = e.touches[0].clientY;
|
| 662 |
+
});
|
| 663 |
+
joystick.addEventListener('touchmove', (e) => {
|
| 664 |
+
if (!joystickActive) return;
|
| 665 |
+
const dx = e.touches[0].clientX - joystickStart.x;
|
| 666 |
+
const dy = e.touches[0].clientY - joystickStart.y;
|
| 667 |
+
const dist = Math.min(75, Math.hypot(dx, dy));
|
| 668 |
+
const angle = Math.atan2(dy, dx);
|
| 669 |
+
joystickMove.x = Math.cos(angle) * dist / 75;
|
| 670 |
+
joystickMove.y = Math.sin(angle) * dist / 75;
|
| 671 |
+
joystickThumb.style.transform = `translate(${Math.cos(angle)*dist-30}px, ${Math.sin(angle)*dist-30}px)`;
|
| 672 |
+
});
|
| 673 |
+
joystick.addEventListener('touchend', () => {
|
| 674 |
+
joystickActive = false;
|
| 675 |
+
joystickMove.x = 0;
|
| 676 |
+
joystickMove.y = 0;
|
| 677 |
+
joystickThumb.style.transform = `translate(-50%, -50%)`;
|
| 678 |
+
});
|
| 679 |
|
| 680 |
+
const lookJoystick = document.getElementById('look-joystick-container');
|
| 681 |
+
const lookJoystickThumb = document.getElementById('look-joystick-container').querySelector('#joystick-thumb');
|
| 682 |
+
let lookActive = false;
|
| 683 |
+
let lookLast = { x: 0, y: 0 };
|
| 684 |
+
|
| 685 |
+
lookJoystick.addEventListener('touchstart', (e) => {
|
| 686 |
+
e.preventDefault();
|
| 687 |
+
lookActive = true;
|
| 688 |
+
lookLast.x = e.touches[0].clientX;
|
| 689 |
+
lookLast.y = e.touches[0].clientY;
|
| 690 |
+
});
|
| 691 |
+
lookJoystick.addEventListener('touchmove', (e) => {
|
| 692 |
+
if (!lookActive) return;
|
| 693 |
+
const dx = e.touches[0].clientX - lookLast.x;
|
| 694 |
+
const dy = e.touches[0].clientY - lookLast.y;
|
| 695 |
+
lookLast.x = e.touches[0].clientX;
|
| 696 |
+
lookLast.y = e.touches[0].clientY;
|
| 697 |
+
|
| 698 |
+
player.rotation.y -= dx * 0.005;
|
| 699 |
+
camera.rotation.x -= dy * 0.005;
|
| 700 |
+
camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, camera.rotation.x));
|
| 701 |
+
});
|
| 702 |
+
lookJoystick.addEventListener('touchend', () => { lookActive = false; });
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
function setupUI() {
|
| 706 |
+
const chatInput = document.getElementById('chat-input');
|
| 707 |
+
chatInput.addEventListener('keydown', (e) => {
|
| 708 |
+
if (e.key === 'Enter') {
|
| 709 |
+
e.preventDefault();
|
| 710 |
+
if(chatInput.value.trim() !== '') {
|
| 711 |
+
socket.emit('chat_message', { token, username, message: chatInput.value });
|
| 712 |
+
chatInput.value = '';
|
| 713 |
+
}
|
| 714 |
}
|
| 715 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
|
| 717 |
+
document.getElementById('info-popup-close').addEventListener('click', () => {
|
| 718 |
+
document.getElementById('info-popup').style.display = 'none';
|
| 719 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
|
| 721 |
+
if (isAdmin) {
|
| 722 |
+
document.getElementById('yt-load-btn').addEventListener('click', () => {
|
| 723 |
+
const url = document.getElementById('yt-url-input').value;
|
| 724 |
+
socket.emit('set_youtube_url', { token, url });
|
| 725 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
}
|
| 727 |
+
document.getElementById('yt-close-btn').addEventListener('click', () => {
|
| 728 |
+
document.getElementById('youtube-overlay').style.display = 'none';
|
| 729 |
+
if(ytPlayer && ytPlayer.pauseVideo) ytPlayer.pauseVideo();
|
| 730 |
+
});
|
| 731 |
+
|
| 732 |
+
window.onYouTubeIframeAPIReady = () => {
|
| 733 |
+
isYtPlayerReady = true;
|
| 734 |
+
};
|
| 735 |
|
| 736 |
+
window.addEventListener('resize', onWindowResize);
|
| 737 |
+
document.addEventListener('click', checkInteraction);
|
| 738 |
+
}
|
| 739 |
|
| 740 |
+
function onWindowResize() {
|
| 741 |
+
camera.aspect = window.innerWidth / window.innerHeight;
|
| 742 |
+
camera.updateProjectionMatrix();
|
| 743 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 744 |
+
}
|
| 745 |
|
| 746 |
+
function checkInteraction(event) {
|
| 747 |
+
if (!controls.isLocked) return;
|
|
|
|
|
|
|
|
|
|
| 748 |
|
| 749 |
+
const raycaster = new THREE.Raycaster();
|
| 750 |
+
const mouse = new THREE.Vector2(0, 0); // Center of screen
|
| 751 |
+
raycaster.setFromCamera(mouse, camera);
|
| 752 |
+
|
| 753 |
+
const intersects = raycaster.intersectObjects(interactiveObjects);
|
| 754 |
+
if (intersects.length > 0) {
|
| 755 |
+
const object = intersects[0].object;
|
| 756 |
+
const data = object.userData;
|
| 757 |
+
if(data.type === 'hotspot') {
|
| 758 |
+
if (data.id === 'yt') {
|
| 759 |
+
document.getElementById('youtube-overlay').style.display = 'flex';
|
| 760 |
+
socket.emit('request_youtube_state', { token });
|
| 761 |
+
} else if (data.id === 'info1') {
|
| 762 |
+
const popup = document.getElementById('info-popup');
|
| 763 |
+
document.getElementById('info-popup-text').innerText = data.details.text;
|
| 764 |
+
popup.style.display = 'block';
|
| 765 |
+
} else if (data.id === 'tp1') {
|
| 766 |
+
player.position.set(data.details.target.x, data.details.target.y, data.details.target.z);
|
| 767 |
+
}
|
| 768 |
}
|
| 769 |
+
}
|
|
|
|
| 770 |
}
|
| 771 |
|
| 772 |
+
function updatePlayer(delta) {
|
| 773 |
+
if (!controls.isLocked && !joystickActive) return;
|
| 774 |
+
|
| 775 |
+
const speed = 10.0;
|
| 776 |
+
const direction = new THREE.Vector3();
|
| 777 |
|
| 778 |
+
const forward = (keys['KeyW'] || joystickMove.y < -0.1) ? 1 : 0;
|
| 779 |
+
const backward = (keys['KeyS'] || joystickMove.y > 0.1) ? 1 : 0;
|
| 780 |
+
const left = (keys['KeyA'] || joystickMove.x < -0.1) ? 1 : 0;
|
| 781 |
+
const right = (keys['KeyD'] || joystickMove.x > 0.1) ? 1 : 0;
|
| 782 |
+
|
| 783 |
+
direction.z = forward - backward;
|
| 784 |
+
direction.x = left - right;
|
| 785 |
+
direction.normalize();
|
| 786 |
+
|
| 787 |
+
if (controls.isLocked) {
|
| 788 |
+
controls.moveRight(direction.x * speed * delta);
|
| 789 |
+
controls.moveForward(direction.z * speed * delta);
|
| 790 |
+
} else {
|
| 791 |
+
player.translateX(-direction.x * speed * delta);
|
| 792 |
+
player.translateZ(-direction.z * speed * delta);
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
player.position.y += playerVelocity.y * delta;
|
| 796 |
+
|
| 797 |
+
if(player.position.y < TILE_SIZE) {
|
| 798 |
+
playerVelocity.y = 0;
|
| 799 |
+
player.position.y = TILE_SIZE;
|
| 800 |
+
playerOnFloor = true;
|
| 801 |
+
}
|
| 802 |
+
if(keys['Space'] && playerOnFloor) {
|
| 803 |
+
playerVelocity.y = 10;
|
| 804 |
+
playerOnFloor = false;
|
| 805 |
+
}
|
| 806 |
+
playerVelocity.y -= 25 * delta;
|
| 807 |
}
|
| 808 |
+
|
| 809 |
+
function updateRemotePlayers(delta) {
|
| 810 |
+
for (const id in remotePlayers) {
|
| 811 |
+
const p = remotePlayers[id];
|
| 812 |
+
p.object.position.lerp(p.targetPos, delta * 10);
|
| 813 |
+
p.object.quaternion.slerp(new THREE.Quaternion().setFromEuler(p.targetRot), delta * 10);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 814 |
}
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
let lastUpdateTime = 0;
|
| 818 |
+
function animate() {
|
| 819 |
+
requestAnimationFrame(animate);
|
| 820 |
+
const delta = clock.getDelta();
|
| 821 |
+
|
| 822 |
+
updatePlayer(delta);
|
| 823 |
+
updateRemotePlayers(delta);
|
| 824 |
+
|
| 825 |
+
const time = performance.now();
|
| 826 |
+
if (time > lastUpdateTime + 100) { // 10hz update
|
| 827 |
+
socket.emit('player_move', {
|
| 828 |
+
token: token,
|
| 829 |
+
pos: {x: player.position.x, y: player.position.y, z: player.position.z},
|
| 830 |
+
rot: {x: camera.rotation.x, y: player.rotation.y, z: camera.rotation.z}
|
| 831 |
+
});
|
| 832 |
+
lastUpdateTime = time;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 833 |
}
|
|
|
|
| 834 |
|
| 835 |
+
renderer.render(scene, camera);
|
| 836 |
+
}
|
|
|
|
| 837 |
|
| 838 |
+
function setupSocketListeners() {
|
| 839 |
+
socket.on('connect', () => {
|
| 840 |
+
socket.emit('join_world', {
|
| 841 |
+
token: token,
|
| 842 |
+
username: username,
|
| 843 |
+
pos: {x: player.position.x, y: player.position.y, z: player.position.z},
|
| 844 |
+
rot: {x: camera.rotation.x, y: player.rotation.y, z: camera.rotation.z}
|
| 845 |
+
});
|
| 846 |
+
});
|
| 847 |
|
| 848 |
+
socket.on('initial_state', (data) => {
|
| 849 |
+
for (const id in data.players) {
|
| 850 |
+
if (id !== socket.id) {
|
| 851 |
+
addRemotePlayer(id, data.players[id]);
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
data.chat.forEach(msg => displayChatMessage(msg.username, msg.message));
|
| 855 |
+
});
|
| 856 |
|
| 857 |
+
socket.on('player_joined', (data) => {
|
| 858 |
+
if (data.id !== socket.id) {
|
| 859 |
+
addRemotePlayer(data.id, data.playerData);
|
| 860 |
+
}
|
| 861 |
+
});
|
| 862 |
+
|
| 863 |
+
socket.on('player_left', (id) => {
|
| 864 |
+
removeRemotePlayer(id);
|
| 865 |
+
});
|
| 866 |
|
| 867 |
+
socket.on('player_moved', (data) => {
|
| 868 |
+
if (remotePlayers[data.id]) {
|
| 869 |
+
remotePlayers[data.id].targetPos.set(data.pos.x, data.pos.y, data.pos.z);
|
| 870 |
+
remotePlayers[data.id].targetRot.set(data.rot.x, data.rot.y, data.rot.z);
|
| 871 |
+
}
|
| 872 |
+
});
|
| 873 |
|
| 874 |
+
socket.on('new_chat_message', (data) => {
|
| 875 |
+
displayChatMessage(data.username, data.message);
|
| 876 |
+
});
|
| 877 |
+
|
| 878 |
+
socket.on('set_youtube_url', (data) => {
|
| 879 |
+
if (isYtPlayerReady && data.url) {
|
| 880 |
+
const videoId = getYouTubeVideoId(data.url);
|
| 881 |
+
if (videoId) {
|
| 882 |
+
if (ytPlayer) {
|
| 883 |
+
ytPlayer.loadVideoById(videoId);
|
| 884 |
+
} else {
|
| 885 |
+
createYtPlayer(videoId);
|
| 886 |
+
}
|
| 887 |
+
}
|
| 888 |
}
|
| 889 |
+
});
|
| 890 |
|
| 891 |
+
socket.on('youtube_state_change', (data) => {
|
| 892 |
+
if (!ytPlayer) return;
|
| 893 |
isHandlingSync = true;
|
| 894 |
+
if (data.action === 'play') {
|
| 895 |
+
if(Math.abs(ytPlayer.getCurrentTime() - data.time) > 1.5) {
|
| 896 |
+
ytPlayer.seekTo(data.time, true);
|
| 897 |
+
}
|
| 898 |
+
ytPlayer.playVideo();
|
| 899 |
+
} else if (data.action === 'pause') {
|
| 900 |
+
ytPlayer.pauseVideo();
|
| 901 |
+
ytPlayer.seekTo(data.time, true);
|
| 902 |
}
|
| 903 |
+
});
|
| 904 |
+
|
| 905 |
+
socket.on('youtube_initial_state', (data) => {
|
| 906 |
+
if (isYtPlayerReady && data.url) {
|
| 907 |
+
const videoId = getYouTubeVideoId(data.url);
|
| 908 |
+
if(videoId) {
|
| 909 |
+
if(!ytPlayer) createYtPlayer(videoId);
|
| 910 |
+
|
| 911 |
+
isHandlingSync = true;
|
| 912 |
+
ytPlayer.cueVideoById(videoId, data.time || 0);
|
| 913 |
+
if (data.isPlaying) {
|
| 914 |
+
setTimeout(()=> ytPlayer.playVideo(), 500);
|
| 915 |
+
}
|
| 916 |
+
}
|
| 917 |
+
}
|
| 918 |
+
});
|
| 919 |
}
|
| 920 |
+
|
| 921 |
+
function displayChatMessage(user, message) {
|
| 922 |
+
const messagesDiv = document.getElementById('chat-messages');
|
| 923 |
+
const p = document.createElement('p');
|
| 924 |
+
p.innerHTML = `<strong>${user}:</strong> ${message}`;
|
| 925 |
+
messagesDiv.appendChild(p);
|
| 926 |
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 927 |
}
|
| 928 |
+
|
| 929 |
+
function getYouTubeVideoId(url) {
|
| 930 |
+
if (!url) return null;
|
| 931 |
+
let videoId = null;
|
| 932 |
+
const youtubeRegex = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
|
| 933 |
+
const match = url.match(youtubeRegex);
|
| 934 |
+
if (match) {
|
| 935 |
+
videoId = match[1];
|
| 936 |
+
}
|
| 937 |
+
return videoId;
|
|
|
|
| 938 |
}
|
| 939 |
|
| 940 |
+
function createYtPlayer(videoId) {
|
| 941 |
+
ytPlayer = new YT.Player('youtube-player-container', {
|
| 942 |
+
height: '100%',
|
| 943 |
+
width: '100%',
|
| 944 |
+
videoId: videoId,
|
| 945 |
+
playerVars: { 'autoplay': 0, 'controls': 1 },
|
| 946 |
+
events: {
|
| 947 |
+
'onStateChange': onPlayerStateChange
|
| 948 |
+
}
|
| 949 |
+
});
|
| 950 |
}
|
|
|
|
| 951 |
|
| 952 |
+
function onPlayerStateChange(event) {
|
| 953 |
+
if (isHandlingSync) {
|
| 954 |
+
isHandlingSync = false;
|
| 955 |
+
return;
|
| 956 |
+
}
|
| 957 |
+
let action = null;
|
| 958 |
+
if (event.data == YT.PlayerState.PLAYING) {
|
| 959 |
+
action = 'play';
|
| 960 |
+
} else if (event.data == YT.PlayerState.PAUSED) {
|
| 961 |
+
action = 'pause';
|
| 962 |
+
}
|
| 963 |
+
if (action) {
|
| 964 |
+
socket.emit('youtube_state_change', {
|
| 965 |
+
token: token,
|
| 966 |
+
action: action,
|
| 967 |
+
time: event.target.getCurrentTime()
|
| 968 |
+
});
|
| 969 |
+
}
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
init();
|
| 973 |
</script>
|
| 974 |
</body>
|
| 975 |
</html>
|
| 976 |
+
''', token=token, username=username, is_admin=is_admin)
|
| 977 |
+
|
| 978 |
+
@socketio.on('join_world')
|
| 979 |
+
def handle_join_world(data):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
token = data['token']
|
| 981 |
username = data['username']
|
| 982 |
+
join_room(token)
|
| 983 |
+
|
| 984 |
+
if token in db['rooms']:
|
| 985 |
+
room = db['rooms'][token]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 986 |
|
| 987 |
+
emit('initial_state', {'players': room['players'], 'chat': room.get('chat', [])}, to=request.sid)
|
| 988 |
+
|
| 989 |
+
room['players'][request.sid] = {
|
| 990 |
'username': username,
|
| 991 |
+
'pos': data['pos'],
|
| 992 |
+
'rot': data['rot'],
|
| 993 |
+
'color': f'#{random.randint(0, 0xFFFFFF):06x}',
|
| 994 |
+
'bodyColor': f'#{random.randint(0, 0xFFFFFF):06x}'
|
| 995 |
}
|
|
|
|
| 996 |
|
| 997 |
+
emit('player_joined', {'id': request.sid, 'playerData': room['players'][request.sid]}, room=token, include_self=False)
|
| 998 |
+
save_db(db)
|
| 999 |
|
| 1000 |
@socketio.on('disconnect')
|
| 1001 |
def handle_disconnect():
|
| 1002 |
+
for token, room in db['rooms'].items():
|
| 1003 |
+
if request.sid in room['players']:
|
| 1004 |
+
del room['players'][request.sid]
|
| 1005 |
+
leave_room(token)
|
| 1006 |
+
emit('player_left', request.sid, room=token)
|
| 1007 |
+
save_db(db)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1008 |
break
|
| 1009 |
|
| 1010 |
+
@socketio.on('player_move')
|
| 1011 |
+
def handle_player_move(data):
|
| 1012 |
+
token = data['token']
|
| 1013 |
+
if token in db['rooms'] and request.sid in db['rooms'][token]['players']:
|
| 1014 |
+
player_data = db['rooms'][token]['players'][request.sid]
|
| 1015 |
+
player_data['pos'] = data['pos']
|
| 1016 |
+
player_data['rot'] = data['rot']
|
| 1017 |
+
emit('player_moved', {'id': request.sid, 'pos': data['pos'], 'rot': data['rot']}, room=token, include_self=False)
|
| 1018 |
+
|
| 1019 |
+
@socketio.on('chat_message')
|
| 1020 |
+
def handle_chat_message(data):
|
| 1021 |
token = data['token']
|
| 1022 |
+
if token in db['rooms']:
|
| 1023 |
+
message_data = {'username': data['username'], 'message': data['message']}
|
| 1024 |
+
if 'chat' not in db['rooms'][token]:
|
| 1025 |
+
db['rooms'][token]['chat'] = []
|
| 1026 |
+
db['rooms'][token]['chat'].append(message_data)
|
| 1027 |
+
if len(db['rooms'][token]['chat']) > 50:
|
| 1028 |
+
db['rooms'][token]['chat'].pop(0)
|
| 1029 |
|
| 1030 |
+
emit('new_chat_message', message_data, room=token)
|
| 1031 |
+
save_db(db)
|
| 1032 |
+
|
| 1033 |
+
def get_youtube_id(url):
|
| 1034 |
+
if not url: return None
|
| 1035 |
+
parsed_url = urlparse(url)
|
| 1036 |
+
if parsed_url.hostname in ('www.youtube.com', 'youtube.com'):
|
| 1037 |
+
if parsed_url.path == '/watch': return parse_qs(parsed_url.query).get('v', [None])[0]
|
| 1038 |
+
if parsed_url.path.startswith(('/embed/', '/v/')): return parsed_url.path.split('/')[2]
|
| 1039 |
+
elif parsed_url.hostname == 'youtu.be': return parsed_url.path[1:]
|
| 1040 |
+
return None
|
| 1041 |
|
| 1042 |
@socketio.on('set_youtube_url')
|
| 1043 |
def handle_set_youtube_url(data):
|
| 1044 |
token = data['token']
|
| 1045 |
url = data['url']
|
| 1046 |
+
if token in db['rooms'] and db['rooms'][token]['admin'] == session.get('username'):
|
| 1047 |
+
video_id = get_youtube_id(url)
|
| 1048 |
+
if video_id:
|
| 1049 |
+
room = db['rooms'][token]
|
| 1050 |
+
room['youtube']['url'] = url
|
| 1051 |
+
room['youtube']['state'] = {'isPlaying': False, 'currentTime': 0, 'last_sync_time': time.time()}
|
| 1052 |
+
save_db(db)
|
| 1053 |
+
emit('set_youtube_url', {'url': url}, room=token)
|
| 1054 |
|
| 1055 |
@socketio.on('youtube_state_change')
|
| 1056 |
def handle_youtube_state_change(data):
|
| 1057 |
token = data['token']
|
| 1058 |
+
if token in db['rooms']:
|
| 1059 |
+
room = db['rooms'][token]
|
| 1060 |
+
state = room['youtube']['state']
|
| 1061 |
+
state['isPlaying'] = (data['action'] == 'play')
|
| 1062 |
+
state['currentTime'] = data.get('time', 0)
|
| 1063 |
+
state['last_sync_time'] = time.time()
|
| 1064 |
+
save_db(db)
|
| 1065 |
emit('youtube_state_change', data, room=token, include_self=False)
|
| 1066 |
|
| 1067 |
@socketio.on('request_youtube_state')
|
| 1068 |
def handle_request_youtube_state(data):
|
| 1069 |
token = data['token']
|
| 1070 |
+
if token in db['rooms']:
|
| 1071 |
+
room = db['rooms'][token]
|
| 1072 |
+
yt_data = room.get('youtube', {})
|
| 1073 |
+
if yt_data.get('url'):
|
| 1074 |
+
state = yt_data['state']
|
| 1075 |
+
elapsed = time.time() - state['last_sync_time']
|
| 1076 |
+
estimated_time = state['currentTime'] + elapsed if state['isPlaying'] else state['currentTime']
|
| 1077 |
+
emit('youtube_initial_state', {
|
| 1078 |
+
'url': yt_data['url'],
|
| 1079 |
+
'time': estimated_time,
|
| 1080 |
+
'isPlaying': state['isPlaying']
|
| 1081 |
+
}, to=request.sid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1082 |
|
| 1083 |
if __name__ == '__main__':
|
| 1084 |
+
if HF_TOKEN:
|
| 1085 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 1086 |
backup_thread.start()
|
| 1087 |
socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True)
|