Aleksmorshen commited on
Commit
e806447
·
verified ·
1 Parent(s): ba96b75

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +477 -556
app.py CHANGED
@@ -119,7 +119,7 @@ games_data = load_json(GAMES_DB, default={
119
  "name": "Дурак",
120
  "description": "Карточная игра.",
121
  "min_players": 2,
122
- "max_players": 10,
123
  "state": {}
124
  }
125
  })
@@ -283,20 +283,16 @@ def dashboard():
283
  session.pop('username', None)
284
  return redirect(url_for('index'))
285
 
286
-
287
  if request.method == 'POST':
288
  action = request.form.get('action')
289
  if action == 'create':
290
  token = generate_token()
291
  rooms[token] = {
292
- 'users': [session['username']],
293
- 'max_users': 10,
294
  'admin': session['username'],
295
  'current_game': None,
296
- 'guests': [],
297
  'youtube_url': None,
298
- 'youtube_state': {'isPlaying': False, 'currentTime': 0, 'last_sync_time': time.time()},
299
- 'player_states': {}
300
  }
301
  users[session['username']]['rooms'].append(token)
302
  save_json(ROOMS_DB, rooms)
@@ -304,14 +300,12 @@ def dashboard():
304
  return redirect(url_for('room', token=token))
305
  elif action == 'join':
306
  token = request.form.get('token')
307
- if token in rooms and len(rooms[token]['users']) + len(rooms[token]['guests']) < rooms[token]['max_users']:
308
- if session['username'] not in rooms[token]['users']:
309
- rooms[token]['users'].append(session['username'])
310
  users[session['username']]['rooms'].append(token)
311
- save_json(ROOMS_DB, rooms)
312
  save_json(USERS_DB, users)
313
  return redirect(url_for('room', token=token))
314
- return "Комната не найдена или переполнена", 404
315
 
316
  return render_template_string('''
317
  <!DOCTYPE html>
@@ -423,158 +417,129 @@ def dashboard():
423
  </html>
424
  ''', session=session)
425
 
426
-
427
  @app.route('/logout', methods=['POST'])
428
  def logout():
429
- username = session.get('username')
430
- if username:
431
- for room_token in list(users.get(username, {}).get('rooms', [])):
432
- if room_token in rooms and (username in rooms[room_token]['users'] or username in rooms[room_token].get('player_states', {})):
433
- if rooms[room_token]['admin'] == username:
434
- del rooms[room_token]
435
- save_json(ROOMS_DB, rooms)
436
- socketio.emit('room_deleted', {'token': room_token}, room=room_token)
437
- else:
438
- if username in rooms[room_token]['users']:
439
- rooms[room_token]['users'].remove(username)
440
- if username in rooms[room_token]['player_states']:
441
- del rooms[room_token]['player_states'][username]
442
- save_json(ROOMS_DB, rooms)
443
- socketio.emit('user_left', {'username': username}, room=room_token)
444
-
445
- if username in users and room_token in users[username]['rooms']:
446
- users[username]['rooms'].remove(room_token)
447
- save_json(USERS_DB, users)
448
  session.pop('username', None)
449
  return redirect(url_for('index'))
450
 
451
-
452
-
453
  @app.route('/room/<token>')
454
  def room(token):
455
- if 'username' not in session and 'guest_id' not in session:
456
- return redirect(url_for('index'))
 
457
 
458
  if token not in rooms:
459
  return redirect(url_for('dashboard'))
460
 
461
- is_admin = rooms[token]['admin'] == session.get('username')
462
- current_game = rooms[token].get('current_game')
463
-
464
- if 'username' in session:
465
- username = session['username']
466
- is_guest = False
467
- elif 'guest_id' in session:
468
- username = session['guest_id']
469
- is_guest = True
470
- if username not in rooms[token]['guests']:
471
- rooms[token]['guests'].append(username)
472
- save_json(ROOMS_DB, rooms)
473
- else:
474
- return redirect(url_for('guest_login', token=token))
475
-
476
-
477
  return render_template_string('''
478
  <!DOCTYPE html>
479
  <html lang="ru">
480
  <head>
481
  <meta charset="UTF-8">
482
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
483
- <title>Метавселенная: Комната {{ token }}</title>
484
  <style>
485
- body { margin: 0; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; }
486
- #webgl-canvas { display: block; }
487
- .ui-overlay {
488
- position: fixed;
489
- top: 0;
490
- left: 0;
491
- width: 100%;
492
- height: 100%;
493
- z-index: 10;
494
- pointer-events: none;
495
- color: white;
496
  }
497
- .top-left-ui { position: absolute; top: 15px; left: 15px; background: rgba(0,0,0,0.5); padding: 10px; border-radius: 8px; pointer-events: auto; }
498
- .top-left-ui h1, .top-left-ui p { margin: 0 0 5px 0; }
499
- .leave-button { background-color: #f44336; color: white; border: none; padding: 8px 12px; border-radius: 5px; cursor: pointer; }
500
- .copy-link-button { background-color: #2196F3; color: white; border: none; padding: 8px 12px; border-radius: 5px; cursor: pointer; margin-top: 5px; }
501
- .center-ui {
502
- position: absolute;
503
- top: 50%;
504
- left: 50%;
505
- transform: translate(-50%, -50%);
506
- background: rgba(0, 0, 0, 0.7);
507
- padding: 20px;
508
- border-radius: 12px;
509
- display: none;
510
- flex-direction: column;
511
- align-items: center;
512
- max-width: 90vw;
513
- max-height: 90vh;
514
- overflow-y: auto;
515
- pointer-events: auto;
516
  }
517
- #game-display, #youtube-player-section { display: none; }
518
- .game-card, .youtube-controls, .game-button, .game-input, .card, .card-container { color: #333; }
519
- .info-popup { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.7); padding: 10px 20px; border-radius: 20px; }
520
- #hidden-videos { display: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  </style>
 
 
 
 
 
522
  </head>
523
  <body>
524
- <div id="hidden-videos"></div>
525
- <canvas id="webgl-canvas"></canvas>
526
-
527
- <div class="ui-overlay">
528
- <div class="top-left-ui">
529
- <h1>Комната: {{ token }}</h1>
530
- <p id="users-list">Пользователи: </p>
531
- <button class="copy-link-button" onclick="copyRoomLink()">Копировать ссылку</button>
532
- <button class="leave-button" onclick="leaveRoom()">Покинуть комнату</button>
533
- </div>
534
-
535
- <div id="game-zone-ui" class="center-ui">
536
- <h2>Игровая Зона</h2>
537
- <div id="game-list" class="game-list">
538
- {% for game_id, game_info in games_data.items() %}
539
- <div class="game-card">
540
- <h3>{{ game_info.name }}</h3>
541
- <p>{{ game_info.description }}</p>
542
- {% if is_admin %}
543
- <button class="start-game-button" onclick="startGame('{{ game_id }}')">Начать</button>
544
- {% endif %}
545
- </div>
546
- {% endfor %}
547
- </div>
548
- <div id="game-display">
549
- <h2></h2>
550
- <p id="game-description"></p>
551
- <div id="game-content"></div>
552
- </div>
553
  </div>
554
-
555
- <div id="cinema-zone-ui" class="center-ui">
556
- <h2>Кинотеатр</h2>
557
- <div id="youtube-player-section">
558
- <div id="youtube-player"></div>
559
- <div class="youtube-controls">
560
- {% if is_admin %}
561
- <input type="text" id="youtube-url-input" placeholder="Ссылка на YouTube">
562
- <button onclick="setYoutubeUrl()">Загрузить</button>
563
- {% else %}
564
- <p>Админ управляет видео.</p>
565
- {% endif %}
566
  </div>
567
  </div>
 
568
  </div>
 
569
 
570
- <div class="info-popup">
571
- <p>Используйте W, A, S, D для движения. Мышь для обзора.</p>
572
- </div>
 
573
  </div>
574
 
575
- <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script>
576
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
577
- <script src="https://www.youtube.com/iframe_api"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
 
579
  <script>
580
  const socket = io();
@@ -582,406 +547,338 @@ def room(token):
582
  const username = '{{ username }}';
583
  const is_guest = {{ is_guest|tojson }};
584
  const isAdmin = {{ is_admin|tojson }};
 
585
  const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
586
  let localStream;
587
- const peers = {};
588
- const avatars = {};
589
-
590
- let scene, camera, renderer, clock, controls;
591
- let player, youtubePlayerApiReady = false;
592
- let playerState = {
593
- position: new THREE.Vector3(0, 1.7, 5),
594
- velocity: new THREE.Vector3(),
595
- rotation: new THREE.Vector2(),
596
- input: { forward: 0, right: 0 }
597
- };
598
-
599
- function init3D() {
600
- scene = new THREE.Scene();
601
- scene.background = new THREE.Color(0x87ceeb);
602
- scene.fog = new THREE.Fog(0x87ceeb, 0, 75);
603
-
604
- camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
605
- camera.position.copy(playerState.position);
606
-
607
- renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('webgl-canvas'), antialias: true });
608
- renderer.setSize(window.innerWidth, window.innerHeight);
609
- renderer.setPixelRatio(window.devicePixelRatio);
610
- renderer.shadowMap.enabled = true;
611
-
612
- clock = new THREE.Clock();
613
-
614
- const floor = new THREE.Mesh(
615
- new THREE.PlaneGeometry(100, 100),
616
- new THREE.MeshStandardMaterial({ color: 0x999999 })
617
- );
618
- floor.rotation.x = -Math.PI / 2;
619
- floor.receiveShadow = true;
620
- scene.add(floor);
621
-
622
- const light = new THREE.DirectionalLight(0xffffff, 1);
623
- light.position.set(5, 10, 7.5);
624
- light.castShadow = true;
625
- scene.add(light);
626
- scene.add(new THREE.AmbientLight(0x404040, 0.5));
627
-
628
- setupInteractiveZones();
629
- setupEventListeners();
630
- animate();
631
- }
632
-
633
- const interactiveZones = {
634
- game: new THREE.Box3(new THREE.Vector3(-15, 0, -15), new THREE.Vector3(-5, 5, -5)),
635
- cinema: new THREE.Box3(new THREE.Vector3(5, 0, -15), new THREE.Vector3(15, 10, -5))
636
- };
637
-
638
- function setupInteractiveZones() {
639
- const gameZoneMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
640
- const gameZoneGeometry = new THREE.BoxGeometry(10, 5, 10);
641
- const gameZoneMesh = new THREE.Mesh(gameZoneGeometry, gameZoneMaterial);
642
- gameZoneMesh.position.set(-10, 2.5, -10);
643
- scene.add(gameZoneMesh);
644
-
645
- const cinemaZoneMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
646
- const cinemaZoneGeometry = new THREE.BoxGeometry(10, 10, 10);
647
- const cinemaZoneMesh = new THREE.Mesh(cinemaZoneGeometry, cinemaZoneMaterial);
648
- cinemaZoneMesh.position.set(10, 5, -10);
649
- scene.add(cinemaZoneMesh);
650
- }
651
-
652
- function checkInteractiveZones() {
653
- const playerPosition = avatars[username]?.mesh.position;
654
- if (!playerPosition) return;
655
-
656
- const inGameZone = interactiveZones.game.containsPoint(playerPosition);
657
- const inCinemaZone = interactiveZones.cinema.containsPoint(playerPosition);
658
-
659
- document.getElementById('game-zone-ui').style.display = inGameZone ? 'flex' : 'none';
660
- document.getElementById('cinema-zone-ui').style.display = inCinemaZone ? 'flex' : 'none';
661
-
662
- if (inCinemaZone && !player && youtubePlayerApiReady) {
663
- createPlayer();
664
- }
665
- }
666
-
667
- function addAvatar(user, initial_state) {
668
- if (avatars[user]) return;
669
-
670
- const avatarGroup = new THREE.Group();
671
- const body = new THREE.Mesh(
672
- new THREE.CylinderGeometry(0.5, 0.5, 1.5, 16),
673
- new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff })
674
- );
675
- body.position.y = 0.75;
676
- body.castShadow = true;
677
- avatarGroup.add(body);
678
-
679
- const head = new THREE.Mesh(
680
- new THREE.SphereGeometry(0.4, 16, 16),
681
- new THREE.MeshStandardMaterial({ color: 0xeeeeee })
682
- );
683
- head.position.y = 1.9;
684
- avatarGroup.add(head);
685
-
686
- const videoContainer = document.getElementById('hidden-videos');
687
- const video = document.createElement('video');
688
- video.id = `video-${user}`;
689
- video.autoplay = true;
690
- video.playsInline = true;
691
- video.muted = (user === username);
692
- videoContainer.appendChild(video);
693
-
694
- const videoTexture = new THREE.VideoTexture(video);
695
- const videoScreen = new THREE.Mesh(
696
- new THREE.PlaneGeometry(1, 0.75),
697
- new THREE.MeshBasicMaterial({ map: videoTexture })
698
- );
699
- videoScreen.position.y = 3;
700
- avatarGroup.add(videoScreen);
701
-
702
- if (initial_state) {
703
- avatarGroup.position.set(initial_state.pos.x, initial_state.pos.y, initial_state.pos.z);
704
- }
705
 
706
- avatars[user] = { mesh: avatarGroup, video: video, videoScreen: videoScreen };
707
- scene.add(avatarGroup);
708
  }
709
 
710
- function removeAvatar(user) {
711
- if (avatars[user]) {
712
- scene.remove(avatars[user].mesh);
713
- avatars[user].video.remove();
714
- delete avatars[user];
715
- if(peers[user]) {
716
- peers[user].pc.close();
717
- delete peers[user];
718
- }
719
- }
720
  }
721
-
722
- function setupEventListeners() {
723
- document.addEventListener('keydown', (e) => {
724
- switch (e.code) {
725
- case 'KeyW': playerState.input.forward = 1; break;
726
- case 'KeyS': playerState.input.forward = -1; break;
727
- case 'KeyA': playerState.input.right = -1; break;
728
- case 'KeyD': playerState.input.right = 1; break;
729
- }
730
- });
731
- document.addEventListener('keyup', (e) => {
732
- switch (e.code) {
733
- case 'KeyW':
734
- case 'KeyS': playerState.input.forward = 0; break;
735
- case 'KeyA':
736
- case 'KeyD': playerState.input.right = 0; break;
737
- }
738
- });
739
- document.addEventListener('mousemove', (e) => {
740
- if (document.pointerLockElement === renderer.domElement) {
741
- playerState.rotation.x -= e.movementY * 0.002;
742
- playerState.rotation.y -= e.movementX * 0.002;
743
- playerState.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, playerState.rotation.x));
744
  }
745
  });
746
- renderer.domElement.addEventListener('click', () => {
747
- renderer.domElement.requestPointerLock();
748
- });
749
  }
750
-
751
- function updatePlayer(delta) {
752
- if (!avatars[username]) return;
753
 
754
- const speed = 5.0;
755
- playerState.velocity.set(playerState.input.right, 0, -playerState.input.forward).normalize().multiplyScalar(speed * delta);
756
-
757
- const playerMesh = avatars[username].mesh;
758
- playerMesh.quaternion.setFromEuler(new THREE.Euler(0, playerState.rotation.y, 0, 'YXZ'));
759
- playerMesh.position.add(playerState.velocity.applyQuaternion(playerMesh.quaternion));
760
 
761
- camera.quaternion.setFromEuler(new THREE.Euler(playerState.rotation.x, playerState.rotation.y, 0, 'YXZ'));
762
- camera.position.copy(playerMesh.position);
763
-
764
- checkInteractiveZones();
765
- updateProximityAudio();
 
 
 
 
766
  }
767
 
768
- function updateProximityAudio() {
769
- if (!avatars[username]) return;
770
- const myPos = avatars[username].mesh.position;
771
- const maxDist = 15;
772
- const minDist = 1;
773
-
774
- for (const user in avatars) {
775
- if (user === username) continue;
776
- const theirPos = avatars[user].mesh.position;
777
- const distance = myPos.distanceTo(theirPos);
778
-
779
- let volume = 0;
780
- if (distance < maxDist) {
781
- volume = 1.0 - (Math.max(0, distance - minDist) / (maxDist - minDist));
782
- }
783
- avatars[user].video.volume = Math.pow(volume, 2);
784
- }
785
  }
786
 
787
- function animate() {
788
- requestAnimationFrame(animate);
789
- const delta = clock.getDelta();
790
- updatePlayer(delta);
791
- renderer.render(scene, camera);
792
  }
793
 
794
- function sendPlayerState() {
795
- if (avatars[username]) {
796
- const pos = avatars[username].mesh.position;
797
- const rot = avatars[username].mesh.quaternion;
798
- socket.emit('update_player_state', {
799
- token,
800
- state: {
801
- pos: { x: pos.x, y: pos.y, z: pos.z },
802
- rot: { x: rot.x, y: rot.y, z: rot.z, w: rot.w }
803
- }
804
- });
805
- }
806
- }
807
-
808
  socket.on('connect', () => {
809
  navigator.mediaDevices.getUserMedia({ video: true, audio: true })
810
  .then(stream => {
811
  localStream = stream;
812
- addAvatar(username, {pos: playerState.position});
813
- avatars[username].video.srcObject = stream;
814
  socket.emit('join', { token, username, is_guest });
815
- setInterval(sendPlayerState, 100);
816
  })
817
  .catch(err => {
818
- console.error("Media access error:", err);
819
- alert("Не удалось получить доступ к камере и микрофону.");
820
  });
821
  });
822
 
823
- socket.on('init_room_state', (data) => {
824
- updateUsersList(data.users, data.guests);
825
- for (const user in data.player_states) {
826
- if (user !== username) {
827
- addAvatar(user, data.player_states[user]);
828
- initiatePeerConnection(user, true);
829
  }
830
  }
 
 
 
831
  });
832
 
833
- socket.on('user_joined', (data) => {
834
- updateUsersList(data.all_users, data.all_guests);
835
- if (data.username !== username) {
836
- addAvatar(data.username, {pos: {x: 0, y: 1.7, z: 0}}); // Default position
837
- initiatePeerConnection(data.username, false);
 
 
 
 
838
  }
839
  });
840
 
841
- socket.on('user_left', (data) => {
842
- removeAvatar(data.username);
843
- updateUsersList(data.users, data.guests);
 
 
 
 
 
 
844
  });
845
 
846
- socket.on('player_state_updated', (data) => {
847
- if (data.username !== username && avatars[data.username]) {
848
- const avatar = avatars[data.username].mesh;
849
- avatar.position.lerp(new THREE.Vector3(data.state.pos.x, data.state.pos.y, data.state.pos.z), 0.3);
850
- avatar.quaternion.slerp(new THREE.Quaternion(data.state.rot.x, data.state.rot.y, data.state.rot.z, data.state.rot.w), 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
851
  }
852
  });
853
 
854
- function updateUsersList(users, guests) {
855
- const userListEl = document.getElementById('users-list');
856
- userListEl.textContent = 'В комнате: ' + [...users, ...guests].join(', ');
857
- }
858
-
859
- function initiatePeerConnection(remoteUser, isPolite) {
860
- const pc = new RTCPeerConnection(iceConfig);
861
- peers[remoteUser] = { pc, polite: isPolite };
862
-
863
- localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
864
 
865
- pc.ontrack = event => {
866
- if (avatars[remoteUser]) {
867
- avatars[remoteUser].video.srcObject = event.streams[0];
868
- }
869
- };
870
 
871
- pc.onicecandidate = event => {
872
  if (event.candidate) {
873
- socket.emit('signal', { to: remoteUser, from: username, token, signal: { candidate: event.candidate } });
874
  }
875
  };
876
 
877
- pc.onnegotiationneeded = async () => {
878
- try {
879
- await pc.setLocalDescription(await pc.createOffer());
880
- socket.emit('signal', { to: remoteUser, from: username, token, signal: pc.localDescription });
881
- } catch (err) {
882
- console.error(err);
 
 
 
 
 
883
  }
884
  };
 
885
  }
886
 
887
- socket.on('signal', async (data) => {
888
- const { from, signal } = data;
889
- if (!peers[from]) initiatePeerConnection(from, true);
890
- const { pc, polite } = peers[from];
 
891
 
892
- try {
893
- if (signal.description) {
894
- const offerCollision = (signal.description.type === 'offer') && (pc.signalingState !== 'stable');
895
- const ignoreOffer = !polite && offerCollision;
896
- if (ignoreOffer) return;
897
-
898
- await pc.setRemoteDescription(signal.description);
899
- if (signal.description.type === 'offer') {
900
- await pc.setLocalDescription(await pc.createAnswer());
901
- socket.emit('signal', { to: from, from: username, token, signal: {description: pc.localDescription} });
 
 
 
 
 
 
902
  }
903
- } else if (signal.candidate) {
904
- await pc.addIceCandidate(signal.candidate);
905
  }
906
- } catch (err) {
907
- console.error(err);
908
  }
909
  });
910
-
911
- function onYouTubeIframeAPIReady() {
912
- youtubePlayerApiReady = true;
913
- }
914
-
915
- function createPlayer() {
916
- if (player) return;
917
- player = new YT.Player('youtube-player', {
918
- height: '360',
919
- width: '640',
920
- events: {
921
- 'onReady': () => socket.emit('request_youtube_state', { token }),
922
- 'onStateChange': (event) => {
923
- if (isAdmin && [YT.PlayerState.PLAYING, YT.PlayerState.PAUSED].includes(event.data)) {
924
- socket.emit('youtube_state_change', {
925
- token,
926
- action: event.data === YT.PlayerState.PLAYING ? 'play' : 'pause',
927
- time: event.target.getCurrentTime()
928
- });
929
- }
930
- }
931
- }
932
- });
933
- }
934
 
 
 
 
 
 
 
 
 
 
 
935
  socket.on('set_youtube_url', (data) => {
936
- if (!player && youtubePlayerApiReady) createPlayer();
937
- if (player) player.loadVideoById(data.videoId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938
  });
939
 
940
  socket.on('youtube_initial_state', (data) => {
941
- if (!player && youtubePlayerApiReady) createPlayer();
942
- if (player && data.videoId) {
943
- player.cueVideoById(data.videoId, data.time);
944
- if (data.isPlaying) player.playVideo();
945
- }
946
  });
947
 
948
- socket.on('youtube_state_change', (data) => {
949
- if (!player) return;
950
- if (data.action === 'play') {
951
- player.seekTo(data.time, true);
952
- player.playVideo();
953
- } else if (data.action === 'pause') {
954
- player.pauseVideo();
955
- player.seekTo(data.time, true);
 
 
 
956
  }
957
- });
958
 
959
- function setYoutubeUrl() {
960
- const url = document.getElementById('youtube-url-input').value;
961
- socket.emit('set_youtube_url', { token, url });
962
- }
 
 
 
 
 
 
 
 
 
963
 
964
- function leaveRoom() {
965
- socket.emit('leave', { token, username, is_guest });
966
- window.location.href = '/dashboard';
 
 
 
 
 
 
 
967
  }
968
 
969
- function copyRoomLink() {
970
- navigator.clipboard.writeText(window.location.href);
971
- alert('Ссылка скопирована!');
972
  }
973
 
974
- window.addEventListener('resize', () => {
975
- camera.aspect = window.innerWidth / window.innerHeight;
976
- camera.updateProjectionMatrix();
977
- renderer.setSize(window.innerWidth, window.innerHeight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  });
979
 
980
- init3D();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
981
  </script>
982
  </body>
983
  </html>
984
- ''', token=token, session=session, is_admin=is_admin, rooms=rooms, games_data=games_data, username=username, is_guest=is_guest)
985
 
986
  @app.route('/join_as_guest/<token>', methods=['GET'])
987
  def join_as_guest(token):
@@ -998,105 +895,90 @@ def guest_login(token):
998
  if token not in rooms:
999
  return "Комната не найдена", 404
1000
  return render_template_string('''
1001
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><title>Вход для гостей</title></head>
1002
- <body><h1>Вход в комнату {{ token }} как гость</h1>
1003
- <a href="{{ url_for('join_as_guest', token=token) }}">Войти как гость</a></body></html>
 
 
1004
  ''', token=token)
1005
 
1006
-
1007
  @socketio.on('join')
1008
  def handle_join(data):
1009
  token = data['token']
1010
  username = data['username']
1011
  is_guest = data.get('is_guest', False)
1012
- join_room(token)
1013
-
1014
  if token in rooms:
1015
- if is_guest:
1016
- if username not in rooms[token]['guests']:
1017
- rooms[token]['guests'].append(username)
1018
- else:
1019
- if username not in rooms[token]['users']:
1020
- rooms[token]['users'].append(username)
1021
-
1022
- initial_state = {
1023
- 'pos': {'x': random.uniform(-5, 5), 'y': 1.7, 'z': random.uniform(-5, 5)},
1024
- 'rot': {'x': 0, 'y': 0, 'z': 0, 'w': 1}
 
 
 
 
 
1025
  }
1026
- rooms[token]['player_states'][username] = initial_state
 
 
1027
  save_json(ROOMS_DB, rooms)
1028
 
1029
- emit('init_room_state', {
1030
- 'player_states': rooms[token]['player_states'],
1031
- 'users': rooms[token]['users'],
1032
- 'guests': rooms[token]['guests']
1033
- }, to=request.sid)
1034
-
1035
- emit('user_joined', {
1036
- 'username': username,
1037
- 'initial_state': initial_state,
1038
- 'all_users': rooms[token]['users'],
1039
- 'all_guests': rooms[token]['guests']
1040
- }, room=token, include_self=False)
1041
-
1042
- @socketio.on('leave')
1043
- def handle_leave(data):
1044
- token = data['token']
1045
- username = data['username']
1046
- is_guest = data.get('is_guest', False)
1047
- leave_room(token)
1048
-
1049
- if token in rooms:
1050
- if is_guest and username in rooms[token]['guests']:
1051
- rooms[token]['guests'].remove(username)
1052
- elif not is_guest and username in rooms[token]['users']:
1053
- rooms[token]['users'].remove(username)
1054
-
1055
- if username in rooms[token]['player_states']:
1056
- del rooms[token]['player_states'][username]
1057
 
1058
- save_json(ROOMS_DB, rooms)
1059
- emit('user_left', {
1060
- 'username': username,
1061
- 'users': rooms[token]['users'],
1062
- 'guests': rooms[token]['guests']
1063
- }, room=token)
1064
 
1065
- @socketio.on('update_player_state')
1066
- def handle_update_player_state(data):
1067
  token = data['token']
1068
- state = data['state']
1069
- username = session.get('username') or session.get('guest_id')
1070
- if token in rooms and username:
1071
- rooms[token]['player_states'][username] = state
1072
- emit('player_state_updated', {
1073
- 'username': username,
1074
- 'state': state
1075
  }, room=token, include_self=False)
1076
-
1077
  @socketio.on('signal')
1078
  def handle_signal(data):
1079
- emit('signal', data, room=data['token'], include_self=False)
 
 
 
1080
 
1081
  @socketio.on('set_youtube_url')
1082
  def handle_set_youtube_url(data):
1083
  token = data['token']
1084
  url = data['url']
1085
- username = session.get('username')
1086
- if token in rooms and rooms[token].get('admin') == username:
1087
- video_id = get_youtube_id(url)
1088
- if video_id:
1089
- rooms[token]['youtube_url'] = url
1090
- rooms[token]['youtube_state'] = {'isPlaying': False, 'currentTime': 0, 'last_sync_time': time.time(), 'videoId': video_id}
1091
- save_json(ROOMS_DB, rooms)
1092
- emit('set_youtube_url', {'videoId': video_id}, room=token)
1093
 
1094
  @socketio.on('youtube_state_change')
1095
  def handle_youtube_state_change(data):
1096
  token = data['token']
1097
  if token in rooms:
1098
  rooms[token]['youtube_state']['isPlaying'] = (data['action'] == 'play')
1099
- rooms[token]['youtube_state']['currentTime'] = data.get('time', 0)
1100
  rooms[token]['youtube_state']['last_sync_time'] = time.time()
1101
  save_json(ROOMS_DB, rooms)
1102
  emit('youtube_state_change', data, room=token, include_self=False)
@@ -1104,14 +986,13 @@ def handle_youtube_state_change(data):
1104
  @socketio.on('request_youtube_state')
1105
  def handle_request_youtube_state(data):
1106
  token = data['token']
1107
- if token in rooms and rooms[token]['youtube_url']:
1108
  state = rooms[token]['youtube_state']
1109
  elapsed = time.time() - state['last_sync_time']
1110
  estimated_time = state['currentTime'] + elapsed if state['isPlaying'] else state['currentTime']
1111
  emit('youtube_initial_state', {
1112
- 'videoId': state.get('videoId'),
1113
- 'time': estimated_time,
1114
- 'isPlaying': state['isPlaying']
1115
  }, to=request.sid)
1116
 
1117
  @socketio.on('start_game')
@@ -1122,33 +1003,73 @@ def handle_start_game(data):
1122
  if token in rooms and rooms[token].get('admin') == username:
1123
  rooms[token]['current_game'] = game_id
1124
  if token not in games_data[game_id]['state']:
1125
- games_data[game_id]['state'][token] = {}
1126
- games_data[game_id]['state'][token]['players'] = rooms[token]['users'] + rooms[token]['guests']
 
 
 
 
1127
  save_json(ROOMS_DB, rooms)
1128
  save_json(GAMES_DB, games_data)
1129
  emit('game_started', {'game_id': game_id}, room=token)
1130
  emit('update_game_state', {'game_id': game_id, 'state': games_data[game_id]['state'][token]}, room=token)
1131
 
1132
- @socketio.on('set_game_state')
1133
- def handle_set_game_state(data):
1134
- token = data['token']
1135
- game_id = data['game_id']
1136
- state = data['state']
1137
- username = session.get('username')
1138
- if token in rooms and rooms[token].get('admin') == username and rooms[token]['current_game'] == game_id:
1139
- games_data[game_id]['state'][token] = state
1140
- save_json(GAMES_DB, games_data)
1141
- emit('update_game_state', {'game_id': game_id, 'state': state}, room=token)
1142
-
1143
  @socketio.on('game_action')
1144
  def handle_game_action(data):
1145
  token = data['token']
1146
- game_id = data['game_id']
1147
- user = data['user']
1148
- if token in rooms and game_id == rooms[token].get('current_game'):
1149
- emit('update_game_state', {'game_id': game_id, 'state': games_data[game_id]['state'][token]}, room=token)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1150
 
1151
  if __name__ == '__main__':
1152
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1153
- backup_thread.start()
 
1154
  socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True)
 
119
  "name": "Дурак",
120
  "description": "Карточная игра.",
121
  "min_players": 2,
122
+ "max_players": 6,
123
  "state": {}
124
  }
125
  })
 
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
  'current_game': None,
 
294
  'youtube_url': None,
295
+ 'youtube_state': {'isPlaying': False, 'currentTime': 0, 'last_sync_time': time.time()}
 
296
  }
297
  users[session['username']]['rooms'].append(token)
298
  save_json(ROOMS_DB, rooms)
 
300
  return redirect(url_for('room', token=token))
301
  elif action == 'join':
302
  token = request.form.get('token')
303
+ if token in rooms:
304
+ if session['username'] not in [p['username'] for p in rooms[token]['players'].values()]:
 
305
  users[session['username']]['rooms'].append(token)
 
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>
 
417
  </html>
418
  ''', session=session)
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('/room/<token>')
426
  def room(token):
427
+ username = session.get('username') or session.get('guest_id')
428
+ if not username:
429
+ return redirect(url_for('guest_login', token=token))
430
 
431
  if token not in rooms:
432
  return redirect(url_for('dashboard'))
433
 
434
+ is_guest = 'guest_id' in session
435
+ is_admin = not is_guest and rooms[token]['admin'] == username
436
+
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  return render_template_string('''
438
  <!DOCTYPE html>
439
  <html lang="ru">
440
  <head>
441
  <meta charset="UTF-8">
442
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
443
+ <title>Метавселенная - Комната {{ token }}</title>
444
  <style>
445
+ :root {
446
+ --primary-color: #4CAF50;
447
+ --secondary-color: #388E3C;
448
+ --background-color: #121212;
449
+ --surface-color: rgba(30, 30, 30, 0.9);
450
+ --text-color: #ffffff;
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
+ .hud-button:hover, .copy-link-button:hover { background-color: var(--primary-color); }
464
+ #admin-panel { background-color: var(--surface-color); padding: 15px; border-radius: var(--border-radius); display: none; flex-direction: column; gap: 10px; }
465
+ #admin-panel input { width: calc(100% - 20px); }
466
+ .game-ui-overlay {
467
+ position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
468
+ background-color: var(--surface-color); color: var(--text-color); padding: 20px; border-radius: var(--border-radius);
469
+ box-shadow: var(--box-shadow); z-index: 100; display: none;
470
+ width: 90%; max-width: 600px; max-height: 80vh; overflow-y: auto; text-align: center;
471
+ }
472
+ #game-display h2 { color: var(--primary-color); }
473
+ #game-content { display: flex; flex-direction: column; align-items: center; gap: 10px; }
474
+ .game-input { padding: 8px; border: 1px solid #ccc; border-radius: var(--border-radius); font-size: 1rem; width: 80%; }
475
+ .game-button { background-color: var(--primary-color); color: white; border: none; padding: 10px 15px; cursor: pointer; transition: background-color 0.2s; font-size: 1rem; }
476
+ .game-button:hover { background-color: var(--secondary-color); }
477
+ .card { width: 60px; height: 90px; border: 1px solid black; border-radius: 5px; display: inline-block; margin: 2px; text-align: center; font-size: 1rem; background-color: white; user-select: none; color: black; }
478
+ #youtube-player-container {
479
+ position: fixed; bottom: 10px; left: 10px; width: 320px; height: 180px;
480
+ border-radius: var(--border-radius); overflow: hidden; z-index: 20; display: none;
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 class="hud">
492
+ <div class="hud-left">
493
+ <button class="hud-button" onclick="document.querySelector('a-scene').exitVR()">Выйти из VR</button>
494
+ <button class="copy-link-button" onclick="copyRoomLink()">Скопировать ссылку</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  </div>
496
+ <div class="hud-right">
497
+ {% if is_admin %}
498
+ <button class="hud-button" onclick="toggleAdminPanel()">Панель админа</button>
499
+ <div id="admin-panel">
500
+ <h4>YouTube</h4>
501
+ <input type="text" id="youtube-url-input" placeholder="Ссылка на YouTube видео">
502
+ <button class="hud-button" onclick="setYoutubeUrl()">Загрузить</button>
503
+ <h4>Игры</h4>
504
+ <div id="games-list">
505
+ {% for game_id, game_info in games_data.items() %}
506
+ <button class="game-button" onclick="startGame('{{ game_id }}')">{{ game_info.name }}</button>
507
+ {% endfor %}
508
  </div>
509
  </div>
510
+ {% endif %}
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();
 
547
  const username = '{{ username }}';
548
  const is_guest = {{ is_guest|tojson }};
549
  const isAdmin = {{ is_admin|tojson }};
550
+ const peers = {};
551
  const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
552
  let localStream;
553
+ let player;
554
+ let isHandlingSync = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
 
556
+ function copyRoomLink() {
557
+ navigator.clipboard.writeText(window.location.href).then(() => alert('Ссылка скопирована!'));
558
  }
559
 
560
+ function toggleAdminPanel() {
561
+ const panel = document.getElementById('admin-panel');
562
+ panel.style.display = panel.style.display === 'block' ? 'none' : 'block';
 
 
 
 
 
 
 
563
  }
564
+
565
+ function onYouTubeIframeAPIReady() {
566
+ player = new YT.Player('youtube-player', {
567
+ events: {
568
+ 'onReady': onPlayerReady,
569
+ 'onStateChange': onPlayerStateChange
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  }
571
  });
 
 
 
572
  }
 
 
 
573
 
574
+ function onPlayerReady(event) {
575
+ socket.emit('request_youtube_state', { token, username });
576
+ }
 
 
 
577
 
578
+ function onPlayerStateChange(event) {
579
+ if (isHandlingSync) { isHandlingSync = false; return; }
580
+ let action = null;
581
+ let currentTime = event.target.getCurrentTime();
582
+ if (event.data === YT.PlayerState.PLAYING) action = 'play';
583
+ else if (event.data === YT.PlayerState.PAUSED) action = 'pause';
584
+ if (action) {
585
+ socket.emit('youtube_state_change', { token, action, time: currentTime });
586
+ }
587
  }
588
 
589
+ function setYoutubeUrl() {
590
+ if (!isAdmin) return;
591
+ const url = document.getElementById('youtube-url-input').value;
592
+ socket.emit('set_youtube_url', { token, url });
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  }
594
 
595
+ function getYouTubeVideoId(url) {
596
+ if (!url) return null;
597
+ const regExp = /^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/;
598
+ const match = url.match(regExp);
599
+ return (match && match[2].length === 11) ? match[2] : null;
600
  }
601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  socket.on('connect', () => {
603
  navigator.mediaDevices.getUserMedia({ video: true, audio: true })
604
  .then(stream => {
605
  localStream = stream;
606
+ const localVideo = document.getElementById('local-video');
607
+ localVideo.srcObject = stream;
608
  socket.emit('join', { token, username, is_guest });
 
609
  })
610
  .catch(err => {
611
+ console.error("Ошибка доступа к камере:", err);
612
+ socket.emit('join', { token, username, is_guest });
613
  });
614
  });
615
 
616
+ socket.on('init_room', (data) => {
617
+ const playersContainer = document.getElementById('players-container');
618
+ for (const sid in data.players) {
619
+ if (sid !== socket.id) {
620
+ createPlayerAvatar(sid, data.players[sid]);
 
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
+ socket.on('player_left', (data) => {
641
+ const playerEntity = document.getElementById(`player-${data.sid}`);
642
+ if (playerEntity) {
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 (data.signal.type === 'offer') {
658
+ peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal))
659
+ .then(() => peerConnection.createAnswer())
660
+ .then(answer => peerConnection.setLocalDescription(answer))
661
+ .then(() => {
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
+ function createPeerConnection(remoteSid) {
672
+ const peerConnection = new RTCPeerConnection(iceConfig);
673
+ peers[remoteSid] = peerConnection;
 
 
 
 
 
 
 
674
 
675
+ if (localStream) {
676
+ localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
677
+ }
 
 
678
 
679
+ peerConnection.onicecandidate = event => {
680
  if (event.candidate) {
681
+ socket.emit('signal', { to: remoteSid, from_sid: socket.id, token, signal: { type: 'candidate', candidate: event.candidate } });
682
  }
683
  };
684
 
685
+ peerConnection.ontrack = event => {
686
+ const remoteVideo = document.createElement('video');
687
+ remoteVideo.id = `video-${remoteSid}`;
688
+ remoteVideo.srcObject = event.streams[0];
689
+ remoteVideo.autoplay = true;
690
+ remoteVideo.playsinline = true;
691
+ document.querySelector('a-assets').appendChild(remoteVideo);
692
+
693
+ const playerHead = document.querySelector(`#player-${remoteSid} .player-head`);
694
+ if (playerHead) {
695
+ playerHead.setAttribute('material', `shader: flat; src: #video-${remoteSid}`);
696
  }
697
  };
698
+ return peerConnection;
699
  }
700
 
701
+ function createPlayerAvatar(sid, playerData) {
702
+ const playersContainer = document.getElementById('players-container');
703
+ let playerEntity = document.createElement('a-entity');
704
+ playerEntity.id = `player-${sid}`;
705
+ playerEntity.setAttribute('position', playerData.position);
706
 
707
+ playerEntity.innerHTML = `
708
+ <a-box class="player-body" position="0 0.85 0" depth="0.4" height="1.0" width="0.8" color="#555"></a-box>
709
+ <a-box class="player-head" position="0 1.6 0" height="0.5" width="0.5" depth="0.5" color="#AAA"></a-box>
710
+ <a-text value="${playerData.username}" position="0 2.0 0" align="center" color="white" width="4"></a-text>
711
+ `;
712
+ playersContainer.appendChild(playerEntity);
713
+ }
714
+
715
+ socket.on('update_movement', (data) => {
716
+ if (data.sid !== socket.id) {
717
+ const playerEntity = document.getElementById(`player-${data.sid}`);
718
+ if (playerEntity) {
719
+ playerEntity.setAttribute('position', data.position);
720
+ const head = playerEntity.querySelector('.player-head');
721
+ if (head) {
722
+ head.setAttribute('rotation', data.rotation);
723
  }
 
 
724
  }
 
 
725
  }
726
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
 
728
+ document.querySelector('a-scene').addEventListener('loaded', () => {
729
+ setInterval(() => {
730
+ const player = document.getElementById('player');
731
+ const camera = player.querySelector('a-camera');
732
+ const position = player.getAttribute('position');
733
+ const rotation = camera.getAttribute('rotation');
734
+ socket.emit('update_movement', { token, position, rotation });
735
+ }, 100);
736
+ });
737
+
738
  socket.on('set_youtube_url', (data) => {
739
+ updateYoutubePlayer(data.url);
740
+ });
741
+
742
+ socket.on('youtube_state_change', (data) => {
743
+ if (player) {
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
  socket.on('youtube_initial_state', (data) => {
760
+ updateYoutubePlayer(data.url, data.state);
 
 
 
 
761
  });
762
 
763
+ function updateYoutubePlayer(url, state = null) {
764
+ const videoId = getYouTubeVideoId(url);
765
+ if (!videoId) return;
766
+
767
+ document.getElementById('youtube-player-container').style.display = 'block';
768
+
769
+ if (!document.getElementById(`youtube-video-asset-${videoId}`)) {
770
+ const videoAsset = document.createElement('video');
771
+ videoAsset.id = `youtube-video-asset-${videoId}`;
772
+ videoAsset.crossOrigin = 'anonymous';
773
+ document.querySelector('a-assets').appendChild(videoAsset);
774
  }
 
775
 
776
+ const screen = document.getElementById('youtube-screen');
777
+ screen.setAttribute('material', `shader: flat; src: #youtube-video-asset-${videoId}`);
778
+
779
+ player.cueVideoById(videoId);
780
+
781
+ player.getIframe().onload = () => {
782
+ const videoEl = document.getElementById(`youtube-video-asset-${videoId}`);
783
+ const ytVideoEl = player.getIframe().contentDocument.querySelector('video');
784
+ if (ytVideoEl && videoEl.srcObject !== ytVideoEl.captureStream()) {
785
+ videoEl.srcObject = ytVideoEl.captureStream();
786
+ videoEl.play();
787
+ }
788
+ }
789
 
790
+ if(state) {
791
+ isHandlingSync = true;
792
+ if(state.isPlaying) {
793
+ player.seekTo(state.currentTime, true);
794
+ player.playVideo();
795
+ } else {
796
+ player.seekTo(state.currentTime, true);
797
+ player.pauseVideo();
798
+ }
799
+ }
800
  }
801
 
802
+ function startGame(gameId) {
803
+ socket.emit('start_game', { token, game_id: gameId });
 
804
  }
805
 
806
+ socket.on('game_started', (data) => {
807
+ const gameId = data.game_id;
808
+ const gameInfo = {{ games_data|tojson }}[gameId];
809
+ const gameDisplay = document.getElementById('game-display');
810
+ gameDisplay.style.display = 'block';
811
+ gameDisplay.querySelector('h2').innerText = gameInfo.name;
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
+ function updateCrocodileState(state) {
856
+ document.getElementById('crocodile-presenter-view').style.display = state.presenter === username ? 'block' : 'none';
857
+ document.getElementById('crocodile-guesser-view').style.display = state.presenter !== username && state.isRunning ? 'block' : 'none';
858
+ document.getElementById('crocodile-admin-view').style.display = isAdmin && !state.isRunning ? 'block' : 'none';
859
+ if(state.presenter === username) document.getElementById('crocodile-word').innerText = state.word;
860
+ document.getElementById('crocodile-timer').innerText = state.isRunning ? `Время: ${state.timer}` : '';
861
+ const guessesDiv = document.getElementById('crocodile-guesses');
862
+ guessesDiv.innerHTML = '<h4>Догадки:</h4>';
863
+ state.guesses.forEach(g => guessesDiv.innerHTML += `<p>${g.user}: ${g.value} (${g.result})</p>`);
864
+ if (state.winner) guessesDiv.innerHTML += `<h4>Победил ${state.winner}! Слово: ${state.word}</h4>`;
865
+ if (state.isTimeUp) guessesDiv.innerHTML += `<h4>Время вышло! Слово: ${state.word}</h4>`;
866
+ }
867
+
868
+ function initAlias(gameId) { initCrocodile(gameId); } // Similar logic
869
+ function updateAliasState(state) { updateCrocodileState(state); } // Similar logic
870
+
871
+ function initMafia(gameId, gameInfo) {
872
+ // Simplified logic for brevity, see full logic in previous answer
873
+ }
874
+ function updateMafiaState(state) {}
875
+
876
+ function initDurak(gameId, gameInfo) {}
877
+ function updateDurakState(state) {}
878
  </script>
879
  </body>
880
  </html>
881
+ ''', token=token, session=session, is_admin=is_admin, games_data=games_data, username=username, is_guest=is_guest)
882
 
883
  @app.route('/join_as_guest/<token>', methods=['GET'])
884
  def join_as_guest(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
  is_guest = data.get('is_guest', False)
910
+ sid = request.sid
911
+
912
  if token in rooms:
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
+ player_data = {
925
+ 'username': username,
926
+ 'is_guest': is_guest,
927
+ 'position': f"{x_pos} 0 {z_pos}"
928
  }
929
+ rooms[token]['players'][sid] = player_data
930
+
931
+ emit('player_joined', {'sid': sid, 'player_data': player_data}, room=token)
932
  save_json(ROOMS_DB, rooms)
933
 
934
+ @socketio.on('disconnect')
935
+ def handle_disconnect():
936
+ sid = request.sid
937
+ for token, room_data in list(rooms.items()):
938
+ if sid in room_data.get('players', {}):
939
+ del rooms[token]['players'][sid]
940
+ emit('player_left', {'sid': sid}, room=token)
941
+
942
+ if not room_data['players']:
943
+ del rooms[token]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
944
 
945
+ save_json(ROOMS_DB, rooms)
946
+ break
 
 
 
 
947
 
948
+ @socketio.on('update_movement')
949
+ def handle_update_movement(data):
950
  token = data['token']
951
+ if token in rooms and request.sid in rooms[token]['players']:
952
+ rooms[token]['players'][request.sid]['position'] = ' '.join(map(str, data['position'].values()))
953
+ emit('update_movement', {
954
+ 'sid': request.sid,
955
+ 'position': data['position'],
956
+ 'rotation': data['rotation']
 
957
  }, room=token, include_self=False)
958
+
959
  @socketio.on('signal')
960
  def handle_signal(data):
961
+ emit('signal', {
962
+ 'from_sid': data['from_sid'],
963
+ 'signal': data['signal']
964
+ }, room=data['to'])
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
+ rooms[token]['youtube_url'] = url
972
+ rooms[token]['youtube_state'] = {'isPlaying': False, 'currentTime': 0, 'last_sync_time': time.time()}
973
+ save_json(ROOMS_DB, rooms)
974
+ emit('set_youtube_url', {'url': url}, room=token)
 
 
 
975
 
976
  @socketio.on('youtube_state_change')
977
  def handle_youtube_state_change(data):
978
  token = data['token']
979
  if token in rooms:
980
  rooms[token]['youtube_state']['isPlaying'] = (data['action'] == 'play')
981
+ rooms[token]['youtube_state']['currentTime'] = data['time']
982
  rooms[token]['youtube_state']['last_sync_time'] = time.time()
983
  save_json(ROOMS_DB, rooms)
984
  emit('youtube_state_change', data, room=token, include_self=False)
 
986
  @socketio.on('request_youtube_state')
987
  def handle_request_youtube_state(data):
988
  token = data['token']
989
+ if token in rooms and rooms[token].get('youtube_url'):
990
  state = rooms[token]['youtube_state']
991
  elapsed = time.time() - state['last_sync_time']
992
  estimated_time = state['currentTime'] + elapsed if state['isPlaying'] else state['currentTime']
993
  emit('youtube_initial_state', {
994
+ 'url': rooms[token]['youtube_url'],
995
+ 'state': {'isPlaying': state['isPlaying'], 'currentTime': estimated_time}
 
996
  }, to=request.sid)
997
 
998
  @socketio.on('start_game')
 
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 HF_TOKEN_WRITE and HF_TOKEN_READ:
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)