Aleksmorshen commited on
Commit
7eea778
·
verified ·
1 Parent(s): 74b2f4a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1093 -647
app.py CHANGED
@@ -83,17 +83,49 @@ def download_db_from_hf():
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
  upload_db_to_hf()
92
- time.sleep(300)
93
 
94
  rooms = load_json(ROOMS_DB)
95
  users = load_json(USERS_DB)
96
- games_data = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  def generate_token():
99
  return ''.join(random.choices(string.ascii_letters + string.digits, k=15))
@@ -258,15 +290,7 @@ def dashboard():
258
  action = request.form.get('action')
259
  if action == 'create':
260
  token = generate_token()
261
- rooms[token] = {
262
- 'users': [session['username']],
263
- 'guests': [],
264
- 'max_users': 10,
265
- 'admin': session['username'],
266
- 'youtube_url': None,
267
- 'youtube_state': {'isPlaying': False, 'currentTime': 0, 'last_sync_time': time.time()},
268
- 'players': {}
269
- }
270
  users[session['username']]['rooms'].append(token)
271
  save_json(ROOMS_DB, rooms)
272
  save_json(USERS_DB, users)
@@ -395,70 +419,31 @@ def dashboard():
395
 
396
  @app.route('/logout', methods=['POST'])
397
  def logout():
398
- username = session.get('username')
399
- if username and username in users:
400
- rooms_to_check = list(users[username].get('rooms', []))
401
- for room_token in rooms_to_check:
402
- if room_token in rooms:
403
- if username in rooms[room_token].get('users', []):
404
- rooms[room_token]['users'].remove(username)
405
- if username in rooms[room_token].get('players', {}):
406
- del rooms[room_token]['players'][username]
407
-
408
- if not rooms[room_token]['users'] and not rooms[room_token]['guests']:
409
- del rooms[room_token]
410
- socketio.close_room(room_token)
411
- elif rooms[room_token].get('admin') == username:
412
- if rooms[room_token]['users']:
413
- rooms[room_token]['admin'] = rooms[room_token]['users'][0]
414
- elif rooms[room_token]['guests']:
415
- # This case is tricky, guest cannot be admin. Maybe delete room.
416
- del rooms[room_token]
417
- socketio.close_room(room_token)
418
- else:
419
- del rooms[room_token]
420
- socketio.close_room(room_token)
421
-
422
- socketio.emit('user_left', {'username': username}, room=room_token)
423
- save_json(ROOMS_DB, rooms)
424
-
425
- users[username]['rooms'] = []
426
- save_json(USERS_DB, users)
427
  session.pop('username', None)
428
  return redirect(url_for('index'))
429
 
430
 
431
-
432
  @app.route('/room/<token>')
433
  def room(token):
434
  if 'username' not in session and 'guest_id' not in session:
435
- is_guest_link = True
436
- else:
437
- is_guest_link = False
438
 
439
  if token not in rooms:
440
  return redirect(url_for('dashboard'))
441
 
442
- is_admin = False
443
- username = None
444
- is_guest = False
445
-
446
- if 'username' in session and session['username'] in rooms[token].get('users', []):
447
- username = session['username']
448
- is_admin = rooms[token]['admin'] == username
449
  elif 'guest_id' in session:
450
  username = session['guest_id']
451
  is_guest = True
452
  if username not in rooms[token]['guests']:
453
- if len(rooms[token]['users']) + len(rooms[token]['guests']) < rooms[token]['max_users']:
454
- rooms[token]['guests'].append(username)
455
- save_json(ROOMS_DB, rooms)
456
- else:
457
- return "Комната переполнена", 403
458
- elif is_guest_link:
459
- return redirect(url_for('guest_login', token=token))
460
  else:
461
- return redirect(url_for('index'))
462
 
463
  return render_template_string('''
464
  <!DOCTYPE html>
@@ -468,640 +453,995 @@ def room(token):
468
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
469
  <title>Метавселенная - Комната {{ token }}</title>
470
  <style>
471
- body { margin: 0; overflow: hidden; background-color: #000; }
472
- #world { position: fixed; top: 0; left: 0; width: 100%; height: 100%; }
473
- #ui-container {
474
- position: fixed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  top: 10px;
476
- left: 10px;
477
- color: white;
478
- font-family: sans-serif;
479
- z-index: 100;
 
480
  display: flex;
481
  flex-direction: column;
482
- gap: 10px;
 
 
483
  }
484
- #ui-container button, #ui-container input {
485
- padding: 8px 12px;
486
- background-color: rgba(0,0,0,0.5);
487
- color: white;
488
- border: 1px solid white;
489
  border-radius: 5px;
490
- cursor: pointer;
491
  }
492
- #joystick-container {
493
- position: fixed;
494
- bottom: 20px;
495
- left: 20px;
496
- width: 150px;
497
- height: 150px;
498
- z-index: 100;
499
- display: none;
 
500
  }
501
- #youtube-container {
502
- position: fixed;
503
  top: 50%;
504
  left: 50%;
 
 
 
 
505
  transform: translate(-50%, -50%);
506
- width: 80vw;
507
- max-width: 800px;
508
- height: 45vw;
509
- max-height: 450px;
510
- background-color: #000;
511
- border: 2px solid #fff;
512
- border-radius: 10px;
513
- z-index: 200;
514
- display: none;
515
- flex-direction: column;
516
  }
517
- #youtube-container #youtube-player { width: 100%; height: 100%; }
518
- #youtube-container .close-btn {
519
  position: absolute;
520
- top: -30px;
521
- right: 0;
522
- background: white;
523
- color: black;
524
- border: none;
525
- border-radius: 50%;
526
- width: 25px;
527
- height: 25px;
528
- cursor: pointer;
529
- font-weight: bold;
530
  }
531
- .touch-surface {
532
- position: fixed;
533
  top: 0;
534
  left: 0;
535
  width: 100%;
536
  height: 100%;
537
- z-index: 99;
538
  display: none;
 
 
 
539
  }
540
- .info-popup {
541
- position: fixed;
542
- bottom: 20px;
543
- left: 50%;
544
- transform: translateX(-50%);
545
- background-color: rgba(0,0,0,0.7);
546
- color: white;
547
- padding: 15px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  border-radius: 10px;
549
- z-index: 150;
550
- display: none;
551
  text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  }
553
- .info-popup.show { display: block; }
554
  </style>
555
  </head>
556
  <body>
557
- <div id="world"></div>
558
-
559
- <div id="ui-container">
560
- <button id="leave-button">Покинуть комнату</button>
561
- <button id="copy-link-button">Копировать ссылку</button>
562
- {% if is_admin %}
563
- <div style="display: flex; gap: 5px;">
564
- <input type="text" id="youtube-url-input" placeholder="Ссылка YouTube">
565
- <button id="set-youtube-url">Загрузить</button>
566
- </div>
567
- {% endif %}
568
  </div>
569
 
570
- <div id="joystick-container"></div>
571
- <div class="touch-surface"></div>
 
 
 
 
 
 
 
 
572
 
573
- <div id="youtube-container">
574
- <button class="close-btn" onclick="hideYoutubePlayer()">X</button>
575
- <div id="youtube-player"></div>
 
 
576
  </div>
577
 
578
- <div id="info-popup">
579
- <p>Подойдите к экрану, чтобы посмотреть видео</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  </div>
581
 
 
 
582
  <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script>
583
- <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
584
  <script type="importmap">
585
  {
586
  "imports": {
587
- "three": "https://unpkg.com/three@0.157.0/build/three.module.js",
588
- "three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
589
  }
590
  }
591
  </script>
592
- <script src="https://cdn.jsdelivr.net/npm/nipplejs@0.10.1/dist/nipplejs.min.js"></script>
593
- <script src="https://www.youtube.com/iframe_api"></script>
594
-
595
  <script type="module">
596
  import * as THREE from 'three';
 
597
 
598
  const socket = io();
599
  const token = '{{ token }}';
600
  const username = '{{ username }}';
601
  const is_guest = {{ is_guest|tojson }};
602
  const isAdmin = {{ is_admin|tojson }};
603
-
604
- let scene, camera, renderer, audioListener;
605
- let player, playerVelocity = new THREE.Vector3();
606
- let controls = { forward: false, backward: false, left: false, right: false };
607
- let peers = {};
608
- const clock = new THREE.Clock();
 
 
 
 
 
 
 
 
 
 
 
 
609
  const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
610
 
 
 
 
 
 
 
611
  let youtubePlayer;
612
- let youtubeHotspot, youtubeScreen;
613
- let isYoutubePlayerReady = false;
614
- let isYoutubeVisible = false;
 
 
615
 
616
  function init() {
 
 
 
617
  scene = new THREE.Scene();
618
  scene.background = new THREE.Color(0x87ceeb);
619
- scene.fog = new THREE.Fog(0x87ceeb, 0, 100);
620
 
621
- camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
622
-
623
- audioListener = new THREE.AudioListener();
624
- camera.add(audioListener);
 
 
 
 
 
 
 
 
 
 
 
 
 
625
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  renderer = new THREE.WebGLRenderer({ antialias: true });
627
- renderer.setSize(window.innerWidth, window.innerHeight);
628
  renderer.setPixelRatio(window.devicePixelRatio);
629
- renderer.shadowMap.enabled = true;
630
- document.getElementById('world').appendChild(renderer.domElement);
631
-
632
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
633
- scene.add(ambientLight);
634
 
635
- const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
636
- dirLight.position.set(50, 50, 50);
637
- dirLight.castShadow = true;
638
- dirLight.shadow.mapSize.width = 2048;
639
- dirLight.shadow.mapSize.height = 2048;
640
- scene.add(dirLight);
641
 
642
- const ground = new THREE.Mesh(
643
- new THREE.BoxGeometry(200, 1, 200),
644
- new THREE.MeshLambertMaterial({ color: 0x4d9c4b })
645
- );
646
- ground.receiveShadow = true;
647
- ground.position.y = -0.5;
648
- scene.add(ground);
649
-
650
- youtubeHotspot = new THREE.Object3D();
651
- youtubeHotspot.position.set(0, 0, -20);
652
- scene.add(youtubeHotspot);
653
-
654
- const screenBack = new THREE.Mesh(
655
- new THREE.BoxGeometry(16.2, 9.2, 0.5),
656
- new THREE.MeshLambertMaterial({ color: 0x333333 })
657
- );
658
- screenBack.position.y = 5.5;
659
- youtubeHotspot.add(screenBack);
660
-
661
- youtubeScreen = new THREE.Mesh(
662
- new THREE.PlaneGeometry(16, 9),
663
- new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide })
664
- );
665
- youtubeScreen.position.y = 5.5;
666
- youtubeScreen.position.z = 0.26;
667
- youtubeHotspot.add(youtubeScreen);
668
-
669
- createPlayer();
670
-
671
- initControls();
672
-
673
- animate();
674
- }
675
-
676
- function createPlayer() {
677
- const head = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshLambertMaterial({ color: 0xffdbac }));
678
- head.position.y = 1.5;
679
- head.castShadow = true;
680
-
681
- const body = new THREE.Mesh(new THREE.BoxGeometry(1, 1.5, 0.5), new THREE.MeshLambertMaterial({ color: 0x0000ff }));
682
- body.position.y = 0.25;
683
- body.castShadow = true;
684
 
685
- player = new THREE.Group();
686
- player.add(head);
687
- player.add(body);
688
- player.position.set(0, 0.5, 5);
689
- scene.add(player);
690
- camera.position.set(0, 5, 10);
691
- camera.lookAt(player.position);
692
  }
693
 
694
- function initControls() {
695
- document.addEventListener('keydown', (e) => {
696
- switch(e.code) {
697
- case 'KeyW': controls.forward = true; break;
698
- case 'KeyS': controls.backward = true; break;
699
- case 'KeyA': controls.left = true; break;
700
- case 'KeyD': controls.right = true; break;
701
- }
702
  });
703
- document.addEventListener('keyup', (e) => {
704
- switch(e.code) {
705
- case 'KeyW': controls.forward = false; break;
706
- case 'KeyS': controls.backward = false; break;
707
- case 'KeyA': controls.left = false; break;
708
- case 'KeyD': controls.right = false; break;
709
- }
710
  });
711
 
712
- let isPointerLocked = false;
713
- document.addEventListener('pointerlockchange', () => {
714
- isPointerLocked = document.pointerLockElement === renderer.domElement;
715
  });
716
- renderer.domElement.addEventListener('click', () => {
717
- if (!isPointerLocked) renderer.domElement.requestPointerLock();
 
 
 
 
718
  });
719
- document.addEventListener('mousemove', (e) => {
720
- if(isPointerLocked) {
721
- player.rotation.y -= e.movementX * 0.002;
722
- camera.rotation.x -= e.movementY * 0.002;
723
- camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, camera.rotation.x));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
725
  });
726
 
727
- if ('ontouchstart' in window) {
728
- document.getElementById('joystick-container').style.display = 'block';
729
- const touchSurface = document.querySelector('.touch-surface');
730
- touchSurface.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
731
 
732
- const joystick = nipplejs.create({
733
- zone: document.getElementById('joystick-container'),
734
- mode: 'static',
735
- position: { left: '85px', bottom: '85px' },
736
- color: 'white'
737
- });
738
 
739
- joystick.on('move', (evt, data) => {
740
- const angle = data.angle.radian;
741
- controls.forward = Math.sin(angle) > 0.1;
742
- controls.backward = Math.sin(angle) < -0.1;
743
- controls.left = Math.cos(angle) < -0.1;
744
- controls.right = Math.cos(angle) > 0.1;
745
- });
746
-
747
- joystick.on('end', () => {
748
- controls.forward = false;
749
- controls.backward = false;
750
- controls.left = false;
751
- controls.right = false;
752
- });
753
 
754
- let touchStartX, touchStartY;
755
- touchSurface.addEventListener('touchstart', (e) => {
756
- if (e.touches.length === 1) {
757
- touchStartX = e.touches[0].clientX;
758
- touchStartY = e.touches[0].clientY;
759
- }
760
- }, {passive: false});
761
-
762
- touchSurface.addEventListener('touchmove', (e) => {
763
- if (e.touches.length === 1) {
764
- e.preventDefault();
765
- const dx = e.touches[0].clientX - touchStartX;
766
- const dy = e.touches[0].clientY - touchStartY;
767
-
768
- player.rotation.y -= dx * 0.005;
769
- camera.rotation.x -= dy * 0.005;
770
- camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, camera.rotation.x));
771
-
772
- touchStartX = e.touches[0].clientX;
773
- touchStartY = e.touches[0].clientY;
774
- }
775
- }, {passive: false});
776
- }
777
  }
778
 
779
- function updatePlayer(delta) {
780
- const speed = 10.0;
781
- const direction = new THREE.Vector3();
782
-
783
- if (controls.forward) direction.z = -1;
784
- if (controls.backward) direction.z = 1;
785
- if (controls.left) direction.x = -1;
786
- if (controls.right) direction.x = 1;
787
-
788
- if (direction.length() > 0) {
789
- direction.normalize().applyQuaternion(player.quaternion);
790
- playerVelocity.add(direction.multiplyScalar(speed * delta));
791
- }
792
-
793
- player.position.add(playerVelocity);
794
- playerVelocity.multiplyScalar(1 - 8.0 * delta);
795
 
796
- const cameraOffset = new THREE.Vector3(0, 4, 8);
797
- cameraOffset.applyQuaternion(player.quaternion);
798
- camera.position.lerp(player.position.clone().add(cameraOffset), 0.1);
799
-
800
- const lookAtTarget = player.position.clone().add(new THREE.Vector3(0, 2, 0));
801
- camera.lookAt(lookAtTarget);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
 
803
- checkHotspots();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
  }
805
 
806
- function checkHotspots() {
807
- const distance = player.position.distanceTo(youtubeHotspot.position);
808
- const popup = document.getElementById('info-popup');
809
- if (distance < 10) {
810
- if (!isYoutubeVisible) {
811
- popup.classList.add('show');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  }
813
- if (distance < 5 && !isYoutubeVisible) {
814
- showYoutubePlayer();
 
815
  }
816
- } else {
817
- popup.classList.remove('show');
818
- if (isYoutubeVisible) {
819
- hideYoutubePlayer();
 
 
 
 
 
 
 
 
 
 
 
820
  }
821
- }
822
- }
823
 
824
- function animate() {
825
- requestAnimationFrame(animate);
826
- const delta = clock.getDelta();
827
-
828
- if (player) {
829
- updatePlayer(delta);
830
- socket.emit('player_moved', {
831
- token,
832
- position: player.position,
833
- rotation: player.rotation,
834
- });
835
  }
 
 
836
 
837
- for (const id in peers) {
838
- const peer = peers[id];
839
- if (peer.mesh && peer.targetPosition && peer.targetRotation) {
840
- peer.mesh.position.lerp(peer.targetPosition, 0.1);
841
- peer.mesh.quaternion.slerp(new THREE.Quaternion().setFromEuler(peer.targetRotation), 0.1);
842
  }
843
  }
844
 
845
  renderer.render(scene, camera);
846
  }
847
-
848
- window.addEventListener('resize', () => {
849
- camera.aspect = window.innerWidth / window.innerHeight;
850
- camera.updateProjectionMatrix();
851
- renderer.setSize(window.innerWidth, window.innerHeight);
852
- });
853
 
854
- function addPeer(id) {
855
- if(peers[id] || id === username) return;
856
-
857
- const head = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshLambertMaterial({ color: 0xffdbac }));
858
- head.position.y = 1.5;
859
- head.castShadow = true;
860
-
861
- const body = new THREE.Mesh(new THREE.BoxGeometry(1, 1.5, 0.5), new THREE.MeshLambertMaterial({ color: 0xff0000 }));
862
- body.position.y = 0.25;
863
- body.castShadow = true;
864
-
865
- const peerMesh = new THREE.Group();
866
- peerMesh.add(head);
867
- peerMesh.add(body);
868
- scene.add(peerMesh);
869
-
870
- const nameTexture = new THREE.CanvasTexture(createTextCanvas(id));
871
- const nameSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: nameTexture, transparent: true }));
872
- nameSprite.position.y = 3.0;
873
- nameSprite.scale.set(3, 1.5, 1);
874
- peerMesh.add(nameSprite);
875
-
876
- const videoPlane = new THREE.Mesh(
877
- new THREE.PlaneGeometry(2, 1.125),
878
- new THREE.MeshBasicMaterial({ color: 0x111111, side: THREE.DoubleSide })
879
- );
880
- videoPlane.position.y = 4.2;
881
- peerMesh.add(videoPlane);
882
-
883
- peers[id] = {
884
- mesh: peerMesh,
885
- videoPlane: videoPlane,
886
- targetPosition: new THREE.Vector3(),
887
- targetRotation: new THREE.Euler(),
888
- pc: new RTCPeerConnection(iceConfig)
889
- };
890
-
891
- const peerConnection = peers[id].pc;
892
-
893
- navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {
894
- stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
895
- }).catch(e => console.error("getUserMedia error", e));
896
-
897
- peerConnection.ontrack = (event) => {
898
- const stream = event.streams[0];
899
- if (stream.getVideoTracks().length > 0) {
900
- const videoEl = document.createElement('video');
901
- videoEl.srcObject = stream;
902
- videoEl.muted = true;
903
- videoEl.play();
904
- const videoTexture = new THREE.VideoTexture(videoEl);
905
- peers[id].videoPlane.material.map = videoTexture;
906
- peers[id].videoPlane.material.needsUpdate = true;
907
- }
908
- if (stream.getAudioTracks().length > 0) {
909
- const peerAudio = new THREE.PositionalAudio(audioListener);
910
- peerAudio.setMediaStreamSource(stream);
911
- peerAudio.setRefDistance(1);
912
- peerAudio.setRolloffFactor(1);
913
- peers[id].mesh.add(peerAudio);
914
- peerAudio.play();
915
- }
916
- };
917
-
918
- peerConnection.onicecandidate = event => {
919
- if (event.candidate) {
920
- socket.emit('signal', { to: id, from: username, token, signal: { type: 'candidate', candidate: event.candidate }});
921
  }
922
- };
 
 
 
 
 
 
 
 
 
 
923
  }
924
 
925
- function removePeer(id) {
926
- if (peers[id]) {
927
- if(peers[id].pc) peers[id].pc.close();
928
- if(peers[id].mesh) scene.remove(peers[id].mesh);
929
- delete peers[id];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
  }
931
  }
932
 
933
- function createTextCanvas(text) {
934
- const canvas = document.createElement('canvas');
935
- const context = canvas.getContext('2d');
936
- canvas.width = 256;
937
- canvas.height = 128;
938
- context.fillStyle = 'rgba(0, 0, 0, 0.7)';
939
- context.fillRect(0, 20, canvas.width, 88);
940
- context.fillStyle = 'white';
941
- context.font = 'bold 32px sans-serif';
942
- context.textAlign = 'center';
943
- context.textBaseline = 'middle';
944
- context.fillText(text, canvas.width / 2, canvas.height / 2);
945
- return canvas;
946
- }
947
-
948
- document.getElementById('leave-button').onclick = () => {
949
- window.location.href = is_guest ? `/guest_login/${token}` : '/dashboard';
950
- };
951
-
952
- document.getElementById('copy-link-button').onclick = () => {
953
- navigator.clipboard.writeText(window.location.href).then(() => alert('Ссылка скопирована!'));
954
- };
955
-
956
- if(isAdmin) {
957
- document.getElementById('set-youtube-url').onclick = () => {
958
- const url = document.getElementById('youtube-url-input').value;
959
- if (url) {
960
- socket.emit('set_youtube_url', { token, url });
961
- }
962
- };
963
  }
964
 
965
- window.onYouTubeIframeAPIReady = () => {
966
- youtubePlayer = new YT.Player('youtube-player', {
967
- height: '100%',
968
- width: '100%',
969
- playerVars: { 'playsinline': 1 },
970
- events: {
971
- 'onReady': () => { isYoutubePlayerReady = true; socket.emit('request_youtube_state', { token }); },
972
- 'onStateChange': onPlayerStateChange
973
- }
974
- });
975
- };
976
 
977
- let isHandlingSync = false;
978
- function onPlayerStateChange(event) {
979
- if (isHandlingSync) { isHandlingSync = false; return; }
980
- let action = null;
981
- if (event.data == YT.PlayerState.PLAYING) action = 'play';
982
- else if (event.data == YT.PlayerState.PAUSED) action = 'pause';
983
- if (action) {
984
- socket.emit('youtube_state_change', {
985
- token, action, time: event.target.getCurrentTime()
986
  });
987
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
988
  }
989
-
990
- function showYoutubePlayer() {
991
- if (!isYoutubePlayerReady) return;
992
- document.getElementById('youtube-container').style.display = 'flex';
993
- isYoutubeVisible = true;
994
- if (document.pointerLockElement) document.exitPointerLock();
995
  }
996
 
997
- window.hideYoutubePlayer = () => {
998
- document.getElementById('youtube-container').style.display = 'none';
999
- isYoutubeVisible = false;
1000
- if (youtubePlayer && typeof youtubePlayer.pauseVideo === 'function') {
1001
- youtubePlayer.pauseVideo();
1002
- }
1003
- };
1004
-
1005
  socket.on('connect', () => {
1006
- socket.emit('join', { token, username, is_guest });
 
 
 
1007
  });
1008
 
1009
- socket.on('init_room', (data) => {
1010
- for (const id in data.players) {
1011
- if (id !== username) {
1012
- addPeer(id);
1013
- peers[id].targetPosition = new THREE.Vector3().fromArray(data.players[id].position);
1014
- peers[id].targetRotation = new THREE.Euler().fromArray(data.players[id].rotation);
1015
- }
 
1016
  }
1017
  });
1018
 
1019
- socket.on('user_joined', async (data) => {
1020
- if (data.username === username) return;
1021
- addPeer(data.username);
1022
- const peerConnection = peers[data.username].pc;
1023
- const offer = await peerConnection.createOffer();
1024
- await peerConnection.setLocalDescription(offer);
1025
- socket.emit('signal', { to: data.username, from: username, token, signal: peerConnection.localDescription });
1026
  });
1027
 
1028
- socket.on('user_left', (data) => {
1029
- removePeer(data.username);
1030
  });
1031
 
1032
- socket.on('player_moved', (data) => {
1033
- if(data.id !== username && peers[data.id]) {
1034
- peers[data.id].targetPosition.set(data.position.x, data.position.y, data.position.z);
1035
- peers[data.id].targetRotation.set(data.rotation._x, data.rotation._y, data.rotation._z, data.rotation._order);
1036
- }
1037
  });
1038
 
1039
- socket.on('signal', async (data) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1040
  if (data.from === username) return;
1041
- if (!peers[data.from]) addPeer(data.from);
1042
-
1043
- const peerConnection = peers[data.from].pc;
1044
- if (data.signal.type === 'offer') {
1045
- await peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal));
1046
- const answer = await peerConnection.createAnswer();
1047
- await peerConnection.setLocalDescription(answer);
1048
- socket.emit('signal', { to: data.from, from: username, token, signal: peerConnection.localDescription });
1049
- } else if (data.signal.type === 'answer') {
1050
- await peerConnection.setRemoteDescription(new RTCSessionDescription(data.signal));
1051
- } else if (data.signal.type === 'candidate') {
1052
- if (peerConnection.remoteDescription) {
1053
- await peerConnection.addIceCandidate(new RTCIceCandidate(data.signal.candidate));
1054
- }
1055
- }
1056
  });
1057
 
1058
- socket.on('set_youtube_url', (data) => {
1059
- if (isYoutubePlayerReady && data.videoId) {
1060
- isHandlingSync = true;
1061
- youtubePlayer.cueVideoById(data.videoId);
1062
- const videoMaterial = new THREE.MeshBasicMaterial({ map: new THREE.VideoTexture(youtubePlayer.getIframe()) });
1063
- youtubeScreen.material = videoMaterial;
1064
  }
1065
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1066
 
1067
- socket.on('youtube_state_change', (data) => {
1068
- if (isYoutubePlayerReady) {
1069
- isHandlingSync = true;
1070
- if (Math.abs(youtubePlayer.getCurrentTime() - data.time) > 1.5) {
1071
- youtubePlayer.seekTo(data.time, true);
1072
- }
1073
- if (data.action === 'play') youtubePlayer.playVideo();
1074
- else if (data.action === 'pause') youtubePlayer.pauseVideo();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1075
  }
1076
  });
1077
-
1078
- socket.on('youtube_initial_state', (data) => {
1079
- if (isYoutubePlayerReady && data.videoId) {
1080
- isHandlingSync = true;
1081
- youtubePlayer.cueVideoById(data.videoId, data.time);
1082
- const iframe = youtubePlayer.getIframe();
1083
- const videoTexture = new THREE.VideoTexture(iframe);
1084
- const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
1085
- youtubeScreen.material = videoMaterial;
1086
-
1087
- setTimeout(() => {
1088
- isHandlingSync = true;
1089
- if (data.isPlaying) youtubePlayer.playVideo();
1090
- else youtubePlayer.pauseVideo();
1091
- }, 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1092
  }
1093
  });
1094
-
1095
- window.addEventListener('beforeunload', () => {
1096
- socket.emit('leave', { token, username, is_guest });
1097
- });
1098
 
1099
- init();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1101
  </script>
1102
  </body>
1103
  </html>
1104
- ''', token=token, session=session, is_admin=is_admin, is_guest=is_guest, username=username)
 
1105
 
1106
  @app.route('/join_as_guest/<token>', methods=['GET'])
1107
  def join_as_guest(token):
@@ -1111,7 +1451,7 @@ def join_as_guest(token):
1111
  guest_id = 'Гость_' + ''.join(random.choices(string.digits, k=4))
1112
  session['guest_id'] = guest_id
1113
 
1114
- if 'username' in session:
1115
  session.pop('username')
1116
 
1117
  return redirect(url_for('room', token=token))
@@ -1142,13 +1482,7 @@ def guest_login(token):
1142
  h1 { color: var(--primary-color); margin-bottom: 20px; }
1143
  p { margin-bottom: 20px; }
1144
  a { padding: 12px 25px; background-color: var(--primary-color); color: white; text-decoration: none; border-radius: 5px; font-size: 1.1rem; transition: background-color 0.3s ease; }
1145
- @media (prefers-color-scheme: dark) {
1146
- :root {
1147
- --background-color: #121212;
1148
- --surface-color: #1e1e1e;
1149
- --text-color: #ffffff;
1150
- }
1151
- }
1152
  </style>
1153
  </head>
1154
  <body>
@@ -1156,127 +1490,239 @@ def guest_login(token):
1156
  <h1>Вход в комнату как Гость</h1>
1157
  <p>Вы можете войти в комнату <strong>{{ token }}</strong> как гость. Ваше имя будет автоматически сгенерировано.</p>
1158
  <a href="{{ url_for('join_as_guest', token=token) }}">Войти в комнату как гость</a>
 
1159
  </div>
1160
  </body>
1161
  </html>
1162
  ''', token=token)
1163
 
 
 
 
 
 
 
 
 
 
1164
  @socketio.on('join')
1165
  def handle_join(data):
1166
  token = data['token']
1167
  username = data['username']
1168
- is_guest = data.get('is_guest', False)
 
 
 
1169
 
1170
- if token in rooms:
1171
- join_room(token)
1172
-
1173
- initial_position = [random.uniform(-10, 10), 0.5, random.uniform(-10, 10)]
1174
- initial_rotation = [0, 0, 0, 'XYZ']
1175
-
1176
- rooms[token]['players'][username] = {
1177
- 'position': initial_position,
1178
- 'rotation': initial_rotation
1179
- }
1180
 
1181
- if is_guest:
1182
- if username not in rooms[token]['guests']:
1183
- rooms[token]['guests'].append(username)
1184
- else:
1185
- if username not in rooms[token]['users']:
1186
- rooms[token]['users'].append(username)
1187
-
1188
- save_json(ROOMS_DB, rooms)
1189
-
1190
- emit('init_room', {'players': rooms[token]['players']}, to=request.sid)
1191
- emit('user_joined', {'username': username, 'state': rooms[token]['players'][username]}, room=token, include_self=False)
1192
 
1193
  @socketio.on('leave')
1194
  def handle_leave(data):
1195
  token = data['token']
1196
  username = data['username']
1197
  is_guest = data.get('is_guest', False)
 
1198
 
1199
  if token in rooms:
1200
- leave_room(token)
1201
-
1202
- if username in rooms[token].get('players', {}):
1203
- del rooms[token]['players'][username]
 
 
1204
 
1205
- if is_guest and username in rooms[token].get('guests', []):
1206
- rooms[token]['guests'].remove(username)
1207
- elif not is_guest and username in rooms[token].get('users', []):
1208
- rooms[token]['users'].remove(username)
1209
-
1210
  if not rooms[token]['users'] and not rooms[token]['guests']:
1211
  del rooms[token]
1212
  else:
1213
- if rooms[token].get('admin') == username and rooms[token]['users']:
1214
- rooms[token]['admin'] = rooms[token]['users'][0]
1215
 
1216
  save_json(ROOMS_DB, rooms)
1217
- emit('user_left', {'username': username}, room=token)
1218
 
1219
- @socketio.on('player_moved')
1220
- def handle_player_moved(data):
1221
  token = data['token']
1222
- username = session.get('username') or session.get('guest_id')
1223
- if token in rooms and username in rooms[token]['players']:
1224
- player_state = rooms[token]['players'][username]
1225
- player_state['position'] = [data['position']['x'], data['position']['y'], data['position']['z']]
1226
- player_state['rotation'] = [data['rotation']['_x'], data['rotation']['_y'], data['rotation']['_z'], data['rotation']['_order']]
1227
-
1228
  emit('player_moved', {
1229
- 'id': username,
1230
- 'position': data['position'],
1231
- 'rotation': data['rotation']
1232
  }, room=token, include_self=False)
1233
 
1234
  @socketio.on('signal')
1235
  def handle_signal(data):
1236
- emit('signal', data, room=data['token'], to=data['to'])
 
 
 
 
1237
 
1238
- @socketio.on('set_youtube_url')
1239
- def handle_set_youtube_url(data):
 
1240
  token = data['token']
1241
- url = data['url']
1242
  username = session.get('username')
1243
 
1244
  if token in rooms and rooms[token].get('admin') == username:
1245
- video_id = get_youtube_id(url)
1246
- if video_id:
1247
- rooms[token]['youtube_url'] = url
1248
- rooms[token]['youtube_state'] = {'isPlaying': False, 'currentTime': 0, 'last_sync_time': time.time()}
1249
- save_json(ROOMS_DB, rooms)
1250
- emit('set_youtube_url', {'videoId': video_id}, room=token)
1251
 
1252
- @socketio.on('youtube_state_change')
1253
- def handle_youtube_state_change(data):
1254
- token = data['token']
1255
- if token in rooms:
1256
- state = rooms[token]['youtube_state']
1257
- state['isPlaying'] = (data['action'] == 'play')
1258
- state['currentTime'] = data['time']
1259
- state['last_sync_time'] = time.time()
1260
  save_json(ROOMS_DB, rooms)
1261
- emit('youtube_state_change', data, room=token, include_self=False)
 
 
1262
 
1263
- @socketio.on('request_youtube_state')
1264
- def handle_request_youtube_state(data):
1265
  token = data['token']
1266
- if token in rooms and rooms[token].get('youtube_url'):
1267
- state = rooms[token]['youtube_state']
1268
- video_id = get_youtube_id(rooms[token]['youtube_url'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1269
 
1270
- elapsed = time.time() - state['last_sync_time']
1271
- estimated_time = state['currentTime'] + elapsed if state['isPlaying'] else state['currentTime']
1272
 
1273
- emit('youtube_initial_state', {
1274
- 'videoId': video_id,
1275
- 'time': estimated_time,
1276
- 'isPlaying': state['isPlaying']
1277
- }, to=request.sid)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1278
 
1279
  if __name__ == '__main__':
 
 
 
 
 
 
 
1280
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1281
- backup_thread.start()
1282
- socketio.run(app, host='0.0.0.0', port=7860, debug=False, allow_unsafe_werkzeug=True)
 
 
 
 
 
 
 
 
83
  json.dump({}, f)
84
  except Exception as e:
85
  print(f"Ошибка при скачивании файла {repo_path}: {e}")
86
+
87
  except Exception as e:
88
  print(f"Ошибка при скачивании файлов с Hugging Face Hub: {e}")
89
 
90
  def periodic_backup():
91
  while True:
92
  upload_db_to_hf()
93
+ time.sleep(15)
94
 
95
  rooms = load_json(ROOMS_DB)
96
  users = load_json(USERS_DB)
97
+ games_data = load_json(GAMES_DB, default={
98
+ "crocodile": {
99
+ "name": "Крокодил",
100
+ "description": "Один игрок показывает слово жестами.",
101
+ "min_players": 2,
102
+ "max_players": 10,
103
+ "state": {}
104
+ },
105
+ "alias": {
106
+ "name": "Alias",
107
+ "description": "Один игрок объясняет слово.",
108
+ "min_players": 2,
109
+ "max_players": 10,
110
+ "state": {}
111
+ },
112
+ "mafia": {
113
+ "name": "Мафия",
114
+ "description": "Мафия против мирных жителей.",
115
+ "min_players": 4,
116
+ "max_players": 10,
117
+ "state": {}
118
+ },
119
+ "durak": {
120
+ "name": "Дурак",
121
+ "description": "Карточная игра.",
122
+ "min_players": 2,
123
+ "max_players": 6,
124
+ "state": {}
125
+ }
126
+ })
127
+ save_json(GAMES_DB, games_data)
128
+
129
 
130
  def generate_token():
131
  return ''.join(random.choices(string.ascii_letters + string.digits, k=15))
 
290
  action = request.form.get('action')
291
  if action == 'create':
292
  token = generate_token()
293
+ rooms[token] = {'users': [session['username']], 'max_users': 10, 'admin': session['username'], 'current_game': None, 'guests': []}
 
 
 
 
 
 
 
 
294
  users[session['username']]['rooms'].append(token)
295
  save_json(ROOMS_DB, rooms)
296
  save_json(USERS_DB, users)
 
419
 
420
  @app.route('/logout', methods=['POST'])
421
  def logout():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  session.pop('username', None)
423
  return redirect(url_for('index'))
424
 
425
 
 
426
  @app.route('/room/<token>')
427
  def room(token):
428
  if 'username' not in session and 'guest_id' not in session:
429
+ return redirect(url_for('index'))
 
 
430
 
431
  if token not in rooms:
432
  return redirect(url_for('dashboard'))
433
 
434
+ is_admin = rooms[token]['admin'] == session.get('username')
435
+
436
+ if 'username' in session:
437
+ username = session['username']
438
+ is_guest = False
 
 
439
  elif 'guest_id' in session:
440
  username = session['guest_id']
441
  is_guest = True
442
  if username not in rooms[token]['guests']:
443
+ rooms[token]['guests'].append(username)
444
+ save_json(ROOMS_DB, rooms)
 
 
 
 
 
445
  else:
446
+ return redirect(url_for('guest_login', token=token))
447
 
448
  return render_template_string('''
449
  <!DOCTYPE html>
 
453
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
454
  <title>Метавселенная - Комната {{ token }}</title>
455
  <style>
456
+ body {
457
+ margin: 0;
458
+ overflow: hidden;
459
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
460
+ background-color: #111;
461
+ color: #fff;
462
+ }
463
+ canvas {
464
+ display: block;
465
+ }
466
+ #blocker {
467
+ position: absolute;
468
+ width: 100%;
469
+ height: 100%;
470
+ background-color: rgba(0,0,0,0.7);
471
+ display: flex;
472
+ justify-content: center;
473
+ align-items: center;
474
+ text-align: center;
475
+ font-size: 24px;
476
+ cursor: pointer;
477
+ }
478
+ #instructions {
479
+ width: 50%;
480
+ background: rgba(20,20,20,0.8);
481
+ padding: 20px;
482
+ border-radius: 10px;
483
+ }
484
+ .video-chat-overlay {
485
+ position: absolute;
486
  top: 10px;
487
+ right: 10px;
488
+ width: 250px;
489
+ background: rgba(0,0,0,0.5);
490
+ border-radius: 8px;
491
+ padding: 5px;
492
  display: flex;
493
  flex-direction: column;
494
+ gap: 5px;
495
+ max-height: 90vh;
496
+ overflow-y: auto;
497
  }
498
+ .video-container {
499
+ position: relative;
500
+ }
501
+ video {
502
+ width: 100%;
503
  border-radius: 5px;
 
504
  }
505
+ .user-indicator {
506
+ position: absolute;
507
+ bottom: 5px;
508
+ left: 5px;
509
+ background: rgba(0,0,0,0.6);
510
+ color: white;
511
+ padding: 2px 5px;
512
+ border-radius: 3px;
513
+ font-size: 12px;
514
  }
515
+ #crosshair {
516
+ position: absolute;
517
  top: 50%;
518
  left: 50%;
519
+ width: 10px;
520
+ height: 10px;
521
+ border: 1px solid white;
522
+ border-radius: 50%;
523
  transform: translate(-50%, -50%);
 
 
 
 
 
 
 
 
 
 
524
  }
525
+ .hotspot-prompt {
 
526
  position: absolute;
527
+ bottom: 20%;
528
+ left: 50%;
529
+ transform: translateX(-50%);
530
+ background: rgba(0,0,0,0.7);
531
+ padding: 10px 20px;
532
+ border-radius: 5px;
533
+ font-size: 16px;
534
+ display: none;
 
 
535
  }
536
+ .modal-overlay {
537
+ position: absolute;
538
  top: 0;
539
  left: 0;
540
  width: 100%;
541
  height: 100%;
542
+ background: rgba(0,0,0,0.8);
543
  display: none;
544
+ justify-content: center;
545
+ align-items: center;
546
+ z-index: 1000;
547
  }
548
+ .modal-content {
549
+ background: #222;
550
+ padding: 20px;
551
+ border-radius: 10px;
552
+ max-width: 90%;
553
+ max-height: 90%;
554
+ overflow-y: auto;
555
+ position: relative;
556
+ }
557
+ .modal-close {
558
+ position: absolute;
559
+ top: 10px;
560
+ right: 15px;
561
+ font-size: 24px;
562
+ cursor: pointer;
563
+ }
564
+ #youtube-player-container {
565
+ width: 80vw;
566
+ height: 45vw;
567
+ max-width: 1280px;
568
+ max-height: 720px;
569
+ }
570
+ #game-display {
571
+ padding: 20px;
572
+ background-color: #333;
573
  border-radius: 10px;
 
 
574
  text-align: center;
575
+ width: 80vw;
576
+ max-width: 600px;
577
+ }
578
+ .game-input, .game-button {
579
+ padding: 10px;
580
+ margin: 5px;
581
+ border-radius: 5px;
582
+ border: 1px solid #555;
583
+ background: #444;
584
+ color: #fff;
585
+ }
586
+ #mobile-controls {
587
+ position: absolute;
588
+ bottom: 0;
589
+ left: 0;
590
+ width: 100%;
591
+ height: 40%;
592
+ display: none;
593
+ z-index: 10;
594
+ }
595
+ #joystick-zone {
596
+ position: absolute;
597
+ bottom: 20px;
598
+ left: 20px;
599
+ width: 120px;
600
+ height: 120px;
601
+ background: rgba(255,255,255,0.2);
602
+ border-radius: 50%;
603
+ }
604
+ #joystick-thumb {
605
+ position: absolute;
606
+ width: 60px;
607
+ height: 60px;
608
+ background: rgba(255,255,255,0.4);
609
+ border-radius: 50%;
610
+ top: 30px;
611
+ left: 30px;
612
+ }
613
+ #look-zone {
614
+ position: absolute;
615
+ bottom: 0;
616
+ right: 0;
617
+ width: 50%;
618
+ height: 100%;
619
+ }
620
+ .leave-button {
621
+ position: absolute;
622
+ top: 20px;
623
+ left: 20px;
624
+ padding: 10px 15px;
625
+ background: #f44336;
626
+ color: white;
627
+ border: none;
628
+ border-radius: 5px;
629
+ cursor: pointer;
630
+ z-index: 20;
631
  }
 
632
  </style>
633
  </head>
634
  <body>
635
+ <div id="blocker">
636
+ <div id="instructions">
637
+ <p style="font-size: 36px;">Нажмите, чтобы войти в метавселенную</p>
638
+ <p>Движение: W, A, S, D<br/>Осмотр: Мышь<br/>Взаимодействие: E</p>
639
+ <p>На мобильных устройствах используйте джойстик и правую часть экрана</p>
640
+ </div>
 
 
 
 
 
641
  </div>
642
 
643
+ <div class="video-chat-overlay" id="video-grid"></div>
644
+ <div id="crosshair">+</div>
645
+ <div class="hotspot-prompt" id="hotspot-prompt">Нажмите [E] для взаимодействия</div>
646
+
647
+ <div class="modal-overlay" id="youtube-modal">
648
+ <div class="modal-content">
649
+ <span class="modal-close" id="youtube-close">×</span>
650
+ <div id="youtube-player-container"></div>
651
+ </div>
652
+ </div>
653
 
654
+ <div class="modal-overlay" id="info-modal">
655
+ <div class="modal-content">
656
+ <span class="modal-close" id="info-close">×</span>
657
+ <p id="info-text"></p>
658
+ </div>
659
  </div>
660
 
661
+ <div class="modal-overlay" id="game-modal">
662
+ <div class="modal-content">
663
+ <span class="modal-close" id="game-close">×</span>
664
+ <div id="game-display">
665
+ <h2></h2>
666
+ <p id="game-description"></p>
667
+ <div id="game-content"></div>
668
+ </div>
669
+ </div>
670
+ </div>
671
+
672
+ <div id="mobile-controls">
673
+ <div id="joystick-zone">
674
+ <div id="joystick-thumb"></div>
675
+ </div>
676
+ <div id="look-zone"></div>
677
  </div>
678
 
679
+ <button class="leave-button" onclick="leaveRoom()">Покинуть комнату</button>
680
+
681
  <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.1/socket.io.js"></script>
682
+ <script src="https://www.youtube.com/iframe_api"></script>
683
  <script type="importmap">
684
  {
685
  "imports": {
686
+ "three": "https://unpkg.com/three@0.165.0/build/three.module.js",
687
+ "three/addons/": "https://unpkg.com/three@0.165.0/examples/jsm/"
688
  }
689
  }
690
  </script>
 
 
 
691
  <script type="module">
692
  import * as THREE from 'three';
693
+ import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
694
 
695
  const socket = io();
696
  const token = '{{ token }}';
697
  const username = '{{ username }}';
698
  const is_guest = {{ is_guest|tojson }};
699
  const isAdmin = {{ is_admin|tojson }};
700
+
701
+ let camera, scene, renderer, controls;
702
+ const objects = [];
703
+ let raycaster;
704
+
705
+ let moveForward = false;
706
+ let moveBackward = false;
707
+ let moveLeft = false;
708
+ let moveRight = false;
709
+ let canJump = false;
710
+
711
+ let prevTime = performance.now();
712
+ const velocity = new THREE.Vector3();
713
+ const direction = new THREE.Vector3();
714
+
715
+ const players = {};
716
+ let localStream;
717
+ const peers = {};
718
  const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
719
 
720
+ const hotspots = [
721
+ { pos: new THREE.Vector3(5, 1.5, -10), type: 'youtube', data: 'dQw4w9WgXcQ', name: 'Секретное видео' },
722
+ { pos: new THREE.Vector3(-8, 1.5, -8), type: 'info', data: 'Добро пожаловать в нашу метавселенную! Исследуйте мир и общайтесь с друзьями.', name: 'Приветствие' },
723
+ { pos: new THREE.Vector3(0, 1.5, 10), type: 'game', data: 'crocodile', name: 'Играть в Крокодила' }
724
+ ];
725
+ let currentHotspot = null;
726
  let youtubePlayer;
727
+
728
+ const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
729
+
730
+ init();
731
+ animate();
732
 
733
  function init() {
734
+ camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
735
+ camera.position.y = 1.6;
736
+
737
  scene = new THREE.Scene();
738
  scene.background = new THREE.Color(0x87ceeb);
739
+ scene.fog = new THREE.Fog(0x87ceeb, 0, 75);
740
 
741
+ const light = new THREE.HemisphereLight(0xeeeeff, 0x777788, 0.9);
742
+ light.position.set(0.5, 1, 0.75);
743
+ scene.add(light);
744
+
745
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
746
+ dirLight.position.set(-1, 1.75, 1);
747
+ dirLight.position.multiplyScalar(30);
748
+ scene.add(dirLight);
749
+
750
+ raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, -1, 0), 0, 10);
751
+
752
+ const mapSize = 20;
753
+ const floorGeometry = new THREE.PlaneGeometry(mapSize*2, mapSize*2, 100, 100);
754
+ const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x4a7d2c });
755
+ const floor = new THREE.Mesh(floorGeometry, floorMaterial);
756
+ floor.rotation.x = -Math.PI / 2;
757
+ scene.add(floor);
758
 
759
+ const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
760
+ const mapLayout = [
761
+ "####################",
762
+ "# #",
763
+ "# #### #### #",
764
+ "# # # # # #",
765
+ "# #### #### #",
766
+ "# #",
767
+ "# ###### #",
768
+ "# # # #",
769
+ "# # # #",
770
+ "# ###### #",
771
+ "# #",
772
+ "# #",
773
+ "# #### #### #",
774
+ "# # # # # #",
775
+ "# # # # # #",
776
+ "# #### #### #",
777
+ "# #",
778
+ "# #",
779
+ "# #",
780
+ "####################",
781
+ ];
782
+
783
+ for(let z=0; z<mapLayout.length; z++) {
784
+ for(let x=0; x<mapLayout[z].length; x++) {
785
+ if (mapLayout[z][x] === '#') {
786
+ const wallMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
787
+ const wall = new THREE.Mesh(boxGeometry, wallMaterial);
788
+ wall.position.set(x - mapSize/2 + 0.5, 0.5, z - mapSize/2 + 0.5);
789
+ scene.add(wall);
790
+ objects.push(wall);
791
+ const wallTop = new THREE.Mesh(boxGeometry, wallMaterial);
792
+ wallTop.position.set(x - mapSize/2 + 0.5, 1.5, z - mapSize/2 + 0.5);
793
+ scene.add(wallTop);
794
+ objects.push(wallTop);
795
+ }
796
+ }
797
+ }
798
+
799
+ hotspots.forEach(hotspot => {
800
+ const hotspotGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.1, 32);
801
+ const hotspotMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.7 });
802
+ const hotspotMesh = new THREE.Mesh(hotspotGeometry, hotspotMaterial);
803
+ hotspotMesh.position.copy(hotspot.pos);
804
+ scene.add(hotspotMesh);
805
+ hotspot.mesh = hotspotMesh;
806
+ });
807
+
808
  renderer = new THREE.WebGLRenderer({ antialias: true });
 
809
  renderer.setPixelRatio(window.devicePixelRatio);
810
+ renderer.setSize(window.innerWidth, window.innerHeight);
811
+ document.body.appendChild(renderer.domElement);
 
 
 
812
 
813
+ if (isMobile) {
814
+ initMobileControls();
815
+ } else {
816
+ initPointerLock();
817
+ }
 
818
 
819
+ document.addEventListener('keydown', onKeyDown);
820
+ document.addEventListener('keyup', onKeyUp);
821
+ window.addEventListener('resize', onWindowResize);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
822
 
823
+ document.getElementById('hotspot-prompt').addEventListener('click', interactWithHotspot);
824
+ document.getElementById('youtube-close').addEventListener('click', closeYoutubeModal);
825
+ document.getElementById('info-close').addEventListener('click', closeInfoModal);
826
+ document.getElementById('game-close').addEventListener('click', closeGameModal);
 
 
 
827
  }
828
 
829
+ function initPointerLock() {
830
+ const blocker = document.getElementById('blocker');
831
+ const instructions = document.getElementById('instructions');
832
+ controls = new PointerLockControls(camera, document.body);
833
+
834
+ instructions.addEventListener('click', function () {
835
+ controls.lock();
 
836
  });
837
+
838
+ controls.addEventListener('lock', function () {
839
+ instructions.style.display = 'none';
840
+ blocker.style.display = 'none';
 
 
 
841
  });
842
 
843
+ controls.addEventListener('unlock', function () {
844
+ blocker.style.display = 'flex';
845
+ instructions.style.display = '';
846
  });
847
+ scene.add(controls.getObject());
848
+ }
849
+
850
+ function initMobileControls() {
851
+ document.getElementById('blocker').addEventListener('click', () => {
852
+ document.getElementById('blocker').style.display = 'none';
853
  });
854
+ document.getElementById('mobile-controls').style.display = 'block';
855
+
856
+ const joystickZone = document.getElementById('joystick-zone');
857
+ const joystickThumb = document.getElementById('joystick-thumb');
858
+ const lookZone = document.getElementById('look-zone');
859
+
860
+ let joystickActive = false;
861
+ let joystickStart = { x: 0, y: 0 };
862
+ const joystickCenter = { x: joystickZone.offsetLeft + 60, y: joystickZone.offsetTop + 60 };
863
+
864
+ joystickZone.addEventListener('touchstart', (e) => {
865
+ e.preventDefault();
866
+ joystickActive = true;
867
+ joystickStart.x = e.touches[0].clientX;
868
+ joystickStart.y = e.touches[0].clientY;
869
+ }, { passive: false });
870
+
871
+ joystickZone.addEventListener('touchmove', (e) => {
872
+ e.preventDefault();
873
+ if (!joystickActive) return;
874
+ let dx = e.touches[0].clientX - joystickStart.x;
875
+ let dy = e.touches[0].clientY - joystickStart.y;
876
+ let dist = Math.sqrt(dx * dx + dy * dy);
877
+ let angle = Math.atan2(dy, dx);
878
+
879
+ if (dist > 30) {
880
+ dx = Math.cos(angle) * 30;
881
+ dy = Math.sin(angle) * 30;
882
  }
883
+
884
+ joystickThumb.style.transform = `translate(${dx}px, ${dy}px)`;
885
+
886
+ moveForward = dy > 10;
887
+ moveBackward = dy < -10;
888
+ moveLeft = dx < -10;
889
+ moveRight = dx > 10;
890
+ }, { passive: false });
891
+
892
+ joystickZone.addEventListener('touchend', (e) => {
893
+ joystickActive = false;
894
+ joystickThumb.style.transform = 'translate(0,0)';
895
+ moveForward = moveBackward = moveLeft = moveRight = false;
896
  });
897
 
898
+ let lookActive = false;
899
+ let lookStart = { x: 0, y: 0 };
900
+
901
+ lookZone.addEventListener('touchstart', (e) => {
902
+ e.preventDefault();
903
+ lookActive = true;
904
+ lookStart.x = e.touches[0].clientX;
905
+ lookStart.y = e.touches[0].clientY;
906
+ }, { passive: false });
907
+
908
+ lookZone.addEventListener('touchmove', (e) => {
909
+ e.preventDefault();
910
+ if (!lookActive) return;
911
+ let dx = e.touches[0].clientX - lookStart.x;
912
+ let dy = e.touches[0].clientY - lookStart.y;
913
 
914
+ camera.rotation.y -= dx * 0.002;
915
+ camera.rotation.x -= dy * 0.002;
916
+ camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, camera.rotation.x));
 
 
 
917
 
918
+ lookStart.x = e.touches[0].clientX;
919
+ lookStart.y = e.touches[0].clientY;
 
 
 
 
 
 
 
 
 
 
 
 
920
 
921
+ }, { passive: false });
922
+
923
+ lookZone.addEventListener('touchend', () => {
924
+ lookActive = false;
925
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
  }
927
 
928
+ function onWindowResize() {
929
+ camera.aspect = window.innerWidth / window.innerHeight;
930
+ camera.updateProjectionMatrix();
931
+ renderer.setSize(window.innerWidth, window.innerHeight);
932
+ }
 
 
 
 
 
 
 
 
 
 
 
933
 
934
+ function onKeyDown(event) {
935
+ switch (event.code) {
936
+ case 'ArrowUp':
937
+ case 'KeyW':
938
+ moveForward = true;
939
+ break;
940
+ case 'ArrowLeft':
941
+ case 'KeyA':
942
+ moveLeft = true;
943
+ break;
944
+ case 'ArrowDown':
945
+ case 'KeyS':
946
+ moveBackward = true;
947
+ break;
948
+ case 'ArrowRight':
949
+ case 'KeyD':
950
+ moveRight = true;
951
+ break;
952
+ case 'Space':
953
+ if (canJump === true) velocity.y += 350;
954
+ canJump = false;
955
+ break;
956
+ case 'KeyE':
957
+ interactWithHotspot();
958
+ break;
959
+ }
960
+ }
961
 
962
+ function onKeyUp(event) {
963
+ switch (event.code) {
964
+ case 'ArrowUp':
965
+ case 'KeyW':
966
+ moveForward = false;
967
+ break;
968
+ case 'ArrowLeft':
969
+ case 'KeyA':
970
+ moveLeft = false;
971
+ break;
972
+ case 'ArrowDown':
973
+ case 'KeyS':
974
+ moveBackward = false;
975
+ break;
976
+ case 'ArrowRight':
977
+ case 'KeyD':
978
+ moveRight = false;
979
+ break;
980
+ }
981
  }
982
 
983
+ function animate() {
984
+ requestAnimationFrame(animate);
985
+ const time = performance.now();
986
+
987
+ let isMoving = false;
988
+ if (isMobile || controls.isLocked === true) {
989
+ const delta = (time - prevTime) / 1000;
990
+
991
+ velocity.x -= velocity.x * 10.0 * delta;
992
+ velocity.z -= velocity.z * 10.0 * delta;
993
+ velocity.y -= 9.8 * 100.0 * delta;
994
+
995
+ direction.z = Number(moveForward) - Number(moveBackward);
996
+ direction.x = Number(moveRight) - Number(moveLeft);
997
+ direction.normalize();
998
+
999
+ if (moveForward || moveBackward) velocity.z -= direction.z * 400.0 * delta;
1000
+ if (moveLeft || moveRight) velocity.x -= direction.x * 400.0 * delta;
1001
+
1002
+ if (isMobile) {
1003
+ camera.translateX(velocity.x * delta);
1004
+ camera.translateZ(velocity.z * delta);
1005
+ } else {
1006
+ controls.moveRight(-velocity.x * delta);
1007
+ controls.moveForward(-velocity.z * delta);
1008
  }
1009
+
1010
+ if (Math.abs(velocity.x) > 0.1 || Math.abs(velocity.z) > 0.1) {
1011
+ isMoving = true;
1012
  }
1013
+
1014
+ if (isMobile) {
1015
+ camera.position.y += (velocity.y * delta);
1016
+ if (camera.position.y < 1.6) {
1017
+ velocity.y = 0;
1018
+ camera.position.y = 1.6;
1019
+ canJump = true;
1020
+ }
1021
+ } else {
1022
+ controls.getObject().position.y += (velocity.y * delta);
1023
+ if (controls.getObject().position.y < 1.6) {
1024
+ velocity.y = 0;
1025
+ controls.getObject().position.y = 1.6;
1026
+ canJump = true;
1027
+ }
1028
  }
 
 
1029
 
1030
+ checkHotspots();
 
 
 
 
 
 
 
 
 
 
1031
  }
1032
+
1033
+ prevTime = time;
1034
 
1035
+ for (const id in players) {
1036
+ if (players[id].mesh && players[id].target) {
1037
+ players[id].mesh.position.lerp(players[id].target.pos, 0.1);
1038
+ players[id].mesh.quaternion.slerp(players[id].target.quat, 0.1);
 
1039
  }
1040
  }
1041
 
1042
  renderer.render(scene, camera);
1043
  }
 
 
 
 
 
 
1044
 
1045
+ function checkHotspots() {
1046
+ let playerPos = isMobile ? camera.position : controls.getObject().position;
1047
+ let closestHotspot = null;
1048
+ let minDistance = 2;
1049
+
1050
+ hotspots.forEach(hotspot => {
1051
+ const distance = playerPos.distanceTo(hotspot.pos);
1052
+ if (distance < minDistance) {
1053
+ closestHotspot = hotspot;
1054
+ minDistance = distance;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1055
  }
1056
+ });
1057
+
1058
+ const prompt = document.getElementById('hotspot-prompt');
1059
+ if (closestHotspot) {
1060
+ prompt.style.display = 'block';
1061
+ prompt.textContent = `Нажмите [E] чтобы: ${closestHotspot.name}`;
1062
+ currentHotspot = closestHotspot;
1063
+ } else {
1064
+ prompt.style.display = 'none';
1065
+ currentHotspot = null;
1066
+ }
1067
  }
1068
 
1069
+ function interactWithHotspot() {
1070
+ if (!currentHotspot) return;
1071
+ if(!isMobile) controls.unlock();
1072
+
1073
+ switch(currentHotspot.type) {
1074
+ case 'youtube':
1075
+ openYoutubeModal(currentHotspot.data);
1076
+ break;
1077
+ case 'info':
1078
+ openInfoModal(currentHotspot.data);
1079
+ break;
1080
+ case 'game':
1081
+ if (isAdmin) {
1082
+ openGameModal(currentHotspot.data);
1083
+ } else {
1084
+ openInfoModal('Только администратор может начать игру.');
1085
+ }
1086
+ break;
1087
+ }
1088
+ }
1089
+
1090
+ function openYoutubeModal(videoId) {
1091
+ document.getElementById('youtube-modal').style.display = 'flex';
1092
+ if (youtubePlayer) {
1093
+ youtubePlayer.loadVideoById(videoId);
1094
+ } else {
1095
+ youtubePlayer = new YT.Player('youtube-player-container', {
1096
+ height: '100%',
1097
+ width: '100%',
1098
+ videoId: videoId,
1099
+ events: { 'onReady': (e) => e.target.playVideo() }
1100
+ });
1101
+ }
1102
+ }
1103
+
1104
+ function closeYoutubeModal() {
1105
+ document.getElementById('youtube-modal').style.display = 'none';
1106
+ if (youtubePlayer && typeof youtubePlayer.stopVideo === 'function') {
1107
+ youtubePlayer.stopVideo();
1108
  }
1109
  }
1110
 
1111
+ function openInfoModal(text) {
1112
+ document.getElementById('info-text').textContent = text;
1113
+ document.getElementById('info-modal').style.display = 'flex';
1114
+ }
1115
+
1116
+ function closeInfoModal() {
1117
+ document.getElementById('info-modal').style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1118
  }
1119
 
1120
+ function openGameModal(gameId) {
1121
+ document.getElementById('game-modal').style.display = 'flex';
1122
+ startGame(gameId);
1123
+ }
1124
+
1125
+ function closeGameModal() {
1126
+ document.getElementById('game-modal').style.display = 'none';
1127
+ }
 
 
 
1128
 
1129
+ setInterval(() => {
1130
+ if (isMobile || (controls && controls.isLocked)) {
1131
+ let playerPos = isMobile ? camera.position : controls.getObject().position;
1132
+ let playerRot = isMobile ? camera.quaternion : controls.getObject().quaternion;
1133
+ socket.emit('player_move', {
1134
+ token: token,
1135
+ username: username,
1136
+ pos: playerPos.toArray(),
1137
+ quat: playerRot.toArray()
1138
  });
1139
  }
1140
+ }, 100);
1141
+
1142
+ function addPlayer(id) {
1143
+ if (players[id] || id === username) return;
1144
+ const playerGroup = new THREE.Group();
1145
+
1146
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff });
1147
+ const body = new THREE.Mesh(new THREE.BoxGeometry(0.8, 1.2, 0.5), bodyMaterial);
1148
+ body.position.y = 0.6;
1149
+
1150
+ const headMaterial = new THREE.MeshStandardMaterial({ color: 0xffdbac });
1151
+ const head = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.4), headMaterial);
1152
+ head.position.y = 1.4;
1153
+
1154
+ const nameCanvas = document.createElement('canvas');
1155
+ const context = nameCanvas.getContext('2d');
1156
+ context.font = "Bold 40px Arial";
1157
+ context.fillStyle = "rgba(255,255,255,0.95)";
1158
+ context.fillText(id, 0, 40);
1159
+ const nameTexture = new THREE.CanvasTexture(nameCanvas);
1160
+ const nameSpriteMaterial = new THREE.SpriteMaterial({ map: nameTexture });
1161
+ const nameSprite = new THREE.Sprite(nameSpriteMaterial);
1162
+ nameSprite.position.y = 2.0;
1163
+ nameSprite.scale.set(2, 1, 1);
1164
+
1165
+ playerGroup.add(body);
1166
+ playerGroup.add(head);
1167
+ playerGroup.add(nameSprite);
1168
+
1169
+ scene.add(playerGroup);
1170
+ players[id] = { mesh: playerGroup, target: { pos: new THREE.Vector3(), quat: new THREE.Quaternion() } };
1171
  }
1172
+
1173
+ function removePlayer(id) {
1174
+ if (players[id]) {
1175
+ scene.remove(players[id].mesh);
1176
+ delete players[id];
1177
+ }
1178
  }
1179
 
 
 
 
 
 
 
 
 
1180
  socket.on('connect', () => {
1181
+ console.log('Connected to server!');
1182
+ if (username || is_guest) {
1183
+ socket.emit('join', { token, username, is_guest });
1184
+ }
1185
  });
1186
 
1187
+ socket.on('player_moved', (data) => {
1188
+ if (data.username === username) return;
1189
+ if (!players[data.username]) {
1190
+ addPlayer(data.username);
1191
+ }
1192
+ if(players[data.username]) {
1193
+ players[data.username].target.pos.fromArray(data.pos);
1194
+ players[data.username].target.quat.fromArray(data.quat);
1195
  }
1196
  });
1197
 
1198
+ socket.on('init_users', (data) => {
1199
+ const allUsers = [...data.users, ...data.guests];
1200
+ allUsers.forEach(user => addPlayer(user));
 
 
 
 
1201
  });
1202
 
1203
+ socket.on('user_joined', (data) => {
1204
+ addPlayer(data.username);
1205
  });
1206
 
1207
+ socket.on('user_left', (data) => {
1208
+ removePlayer(data.username);
 
 
 
1209
  });
1210
 
1211
+ navigator.mediaDevices.getUserMedia({ video: true, audio: true })
1212
+ .then(stream => {
1213
+ localStream = stream;
1214
+ addVideoStream(stream, username, true);
1215
+ })
1216
+ .catch(err => {
1217
+ console.error("Ошибка доступа к медиаустройствам:", err);
1218
+ });
1219
+
1220
+ function addVideoStream(stream, user, isLocal = false) {
1221
+ const videoGrid = document.getElementById('video-grid');
1222
+ const videoContainer = document.createElement('div');
1223
+ videoContainer.classList.add('video-container');
1224
+
1225
+ const video = document.createElement('video');
1226
+ video.srcObject = stream;
1227
+ video.dataset.user = user;
1228
+ video.setAttribute('playsinline', '');
1229
+ video.setAttribute('autoplay', '');
1230
+ if (isLocal) video.muted = true;
1231
+
1232
+ const nameTag = document.createElement('div');
1233
+ nameTag.classList.add('user-indicator');
1234
+ nameTag.textContent = user;
1235
+
1236
+ videoContainer.appendChild(video);
1237
+ videoContainer.appendChild(nameTag);
1238
+ videoGrid.appendChild(videoContainer);
1239
+ video.play();
1240
+ }
1241
+
1242
+ socket.on('signal', data => {
1243
  if (data.from === username) return;
1244
+ handleSignal(data.from, data.signal);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1245
  });
1246
 
1247
+ function handleSignal(from, signal) {
1248
+ if (!peers[from]) {
1249
+ createPeerConnection(from, false);
 
 
 
1250
  }
1251
+ peers[from].signal(signal);
1252
+ }
1253
+
1254
+ function createPeerConnection(remoteUser, initiator) {
1255
+ const peer = new SimplePeer({
1256
+ initiator: initiator,
1257
+ trickle: false,
1258
+ stream: localStream,
1259
+ config: iceConfig
1260
+ });
1261
+
1262
+ peer.on('signal', signal => {
1263
+ socket.emit('signal', { to: remoteUser, from: username, token, signal });
1264
+ });
1265
+
1266
+ peer.on('stream', stream => {
1267
+ addVideoStream(stream, remoteUser);
1268
+ });
1269
+
1270
+ peer.on('close', () => {
1271
+ const videoElement = document.querySelector(`video[data-user="${remoteUser}"]`);
1272
+ if (videoElement) videoElement.parentElement.remove();
1273
+ delete peers[remoteUser];
1274
+ });
1275
+
1276
+ peers[remoteUser] = peer;
1277
+ }
1278
 
1279
+ function leaveRoom() {
1280
+ socket.emit('leave', { token, username, is_guest });
1281
+ window.location.href = is_guest ? '/guest_login/' + token : '/dashboard';
1282
+ }
1283
+ window.leaveRoom = leaveRoom;
1284
+
1285
+
1286
+ // Game Logic Integration
1287
+ function startGame(gameId) {
1288
+ console.log('Starting game:', gameId);
1289
+ document.getElementById('game-display').style.display = 'block';
1290
+ document.getElementById('game-content').innerHTML = '';
1291
+ socket.emit('start_game', { token, game_id: gameId });
1292
+ }
1293
+ window.startGame = startGame;
1294
+
1295
+ socket.on('game_started', (data) => {
1296
+ const gameId = data.game_id;
1297
+ const gameInfo = {{ games_data|tojson }}[gameId];
1298
+ const gameDisplay = document.getElementById('game-display');
1299
+ gameDisplay.style.display = 'block';
1300
+ document.getElementById('game-description').innerText = gameInfo.description;
1301
+ gameDisplay.querySelector('h2').innerText = gameInfo.name;
1302
+
1303
+ const gameContent = document.getElementById('game-content');
1304
+ gameContent.innerHTML = '';
1305
+
1306
+ if (gameId === 'crocodile' || gameId === 'alias'){
1307
+ initWordGame(gameId, gameContent);
1308
+ } else if(gameId === 'mafia'){
1309
+ initMafia(gameContent, gameInfo);
1310
  }
1311
  });
1312
+
1313
+ function initWordGame(gameId, gameContent) {
1314
+ let elements = `
1315
+ <input type="text" id="${gameId}-word-input" placeholder="Введите слово (только админ)" class="game-input">
1316
+ <button id="start-turn-button" class="game-button">Начать ход</button>
1317
+ <input type="text" id="${gameId}-guess-input" placeholder="Ваша догадка" class="game-input">
1318
+ <button id="${gameId}-guess-button" class="game-button">Угадать</button>
1319
+ <div id="${gameId}-result"></div>
1320
+ <div id="${gameId}-timer"></div>
1321
+ `;
1322
+ gameContent.innerHTML = elements;
1323
+
1324
+ const wordInput = document.getElementById(`${gameId}-word-input`);
1325
+ const startTurnButton = document.getElementById('start-turn-button');
1326
+ const guessInput = document.getElementById(`${gameId}-guess-input`);
1327
+ const guessButton = document.getElementById(`${gameId}-guess-button`);
1328
+
1329
+ wordInput.style.display = isAdmin ? 'block' : 'none';
1330
+ startTurnButton.style.display = isAdmin ? 'block' : 'none';
1331
+
1332
+ startTurnButton.onclick = () => {
1333
+ const word = wordInput.value.trim();
1334
+ if (word) {
1335
+ socket.emit('set_game_state', {token, game_id: gameId, state: { word: word, presenter: null, guesses: [], timer: 60, isRunning: true }});
1336
+ wordInput.value = '';
1337
+ }
1338
+ };
1339
+
1340
+ guessButton.onclick = () => {
1341
+ const guess = guessInput.value.trim();
1342
+ if (guess) {
1343
+ socket.emit('game_action', { token, game_id: gameId, action: 'guess', value: guess, user: username });
1344
+ guessInput.value = '';
1345
+ }
1346
+ };
1347
+ }
1348
+
1349
+ function initMafia(gameContent, gameInfo) {
1350
+ let elements = `
1351
+ <button id="start-mafia-button" class="game-button">Начать игру (только админ)</button>
1352
+ <div id="mafia-result"></div>
1353
+ <input type="text" id="mafia-vote-input" placeholder="За кого голосуете?" class="game-input">
1354
+ <button id="mafia-vote-button" class="game-button">Голосовать</button>
1355
+ `;
1356
+ gameContent.innerHTML = elements;
1357
+
1358
+ const startButton = document.getElementById('start-mafia-button');
1359
+ const voteInput = document.getElementById('mafia-vote-input');
1360
+ const voteButton = document.getElementById('mafia-vote-button');
1361
+
1362
+ startButton.style.display = isAdmin ? 'block' : 'none';
1363
+ startButton.onclick = () => socket.emit('set_game_state', { token, game_id: 'mafia', state: { roles: {}, phase: 'night', votes: {}, isRunning: true, killed: null } });
1364
+
1365
+ voteButton.onclick = () => {
1366
+ const vote = voteInput.value.trim();
1367
+ if (vote) {
1368
+ socket.emit('game_action', { token, game_id: 'mafia', action: 'vote', value: vote, user: username });
1369
+ voteInput.value = '';
1370
+ }
1371
+ };
1372
+ }
1373
+
1374
+ socket.on('update_game_state', (data) => {
1375
+ const gameId = data.game_id;
1376
+ const gameState = data.state;
1377
+
1378
+ if (gameId === 'crocodile' || gameId === 'alias'){
1379
+ updateWordGameState(gameId, gameState);
1380
+ } else if (gameId === 'mafia') {
1381
+ updateMafiaState(gameState);
1382
  }
1383
  });
 
 
 
 
1384
 
1385
+ function updateWordGameState(gameId, gameState) {
1386
+ const resultDiv = document.getElementById(`${gameId}-result`);
1387
+ const timerDiv = document.getElementById(`${gameId}-timer`);
1388
+ const guessInput = document.getElementById(`${gameId}-guess-input`);
1389
+ const guessButton = document.getElementById(`${gameId}-guess-button`);
1390
+ if (!resultDiv) return;
1391
+
1392
+ resultDiv.innerHTML = '';
1393
+ if(gameState.presenter) {
1394
+ resultDiv.innerHTML += `<p>Ведущий: ${gameState.presenter}</p>`;
1395
+ }
1396
+ if (username === gameState.presenter) {
1397
+ resultDiv.innerHTML += `<p>Ваше слово: <strong>${gameState.word}</strong></p>`;
1398
+ }
1399
+ gameState.guesses.forEach(g => {
1400
+ resultDiv.innerHTML += `<p>${g.user}: ${g.value} (${g.result})</p>`;
1401
+ });
1402
+ timerDiv.textContent = `Время: ${gameState.timer}`;
1403
+
1404
+ const isGameOver = !gameState.isRunning || gameState.timer <= 0;
1405
+ guessInput.disabled = isGameOver || username === gameState.presenter;
1406
+ guessButton.disabled = isGameOver || username === gameState.presenter;
1407
+
1408
+ if (isGameOver && gameState.word) {
1409
+ resultDiv.innerHTML += `<p>Игра окончена! Загаданное слово было: <strong>${gameState.word}</strong></p>`;
1410
+ }
1411
+ }
1412
 
1413
+ function updateMafiaState(gameState) {
1414
+ const resultDiv = document.getElementById('mafia-result');
1415
+ const voteInput = document.getElementById('mafia-vote-input');
1416
+ const voteButton = document.getElementById('mafia-vote-button');
1417
+ if (!resultDiv) return;
1418
+
1419
+ resultDiv.innerHTML = '';
1420
+ if (gameState.roles && gameState.roles[username]) {
1421
+ resultDiv.innerHTML += `<p>Ваша роль: ${gameState.roles[username]}</p>`;
1422
+ }
1423
+ resultDiv.innerHTML += `<p>Фаза: ${gameState.phase === 'day' ? 'День' : 'Ночь'}</p>`;
1424
+ if (gameState.phase === 'day') {
1425
+ if (gameState.killed) {
1426
+ resultDiv.innerHTML += `<p>Ночью был убит: ${gameState.killed}</p>`;
1427
+ }
1428
+ resultDiv.innerHTML += `<p>Идет голосование. Живые игроки: ${gameState.players.join(', ')}</p>`;
1429
+ voteInput.disabled = false;
1430
+ voteButton.disabled = false;
1431
+ } else {
1432
+ resultDiv.innerHTML += `<p>Мафия выбирает жертву. Мирные спят.</p>`;
1433
+ voteInput.disabled = true;
1434
+ voteButton.disabled = true;
1435
+ }
1436
+ if (gameState.winner) {
1437
+ resultDiv.innerHTML += `<h2>Игра окончена! Победили: ${gameState.winner}</h2>`;
1438
+ }
1439
+ }
1440
  </script>
1441
  </body>
1442
  </html>
1443
+ ''', token=token, session=session, is_admin=is_admin, games_data=games_data, username=username, is_guest=is_guest)
1444
+
1445
 
1446
  @app.route('/join_as_guest/<token>', methods=['GET'])
1447
  def join_as_guest(token):
 
1451
  guest_id = 'Гость_' + ''.join(random.choices(string.digits, k=4))
1452
  session['guest_id'] = guest_id
1453
 
1454
+ if 'username' in session:
1455
  session.pop('username')
1456
 
1457
  return redirect(url_for('room', token=token))
 
1482
  h1 { color: var(--primary-color); margin-bottom: 20px; }
1483
  p { margin-bottom: 20px; }
1484
  a { padding: 12px 25px; background-color: var(--primary-color); color: white; text-decoration: none; border-radius: 5px; font-size: 1.1rem; transition: background-color 0.3s ease; }
1485
+ a:hover { background-color: #388E3C; }
 
 
 
 
 
 
1486
  </style>
1487
  </head>
1488
  <body>
 
1490
  <h1>Вход в комнату как Гость</h1>
1491
  <p>Вы можете войти в комнату <strong>{{ token }}</strong> как гость. Ваше имя будет автоматически сгенерировано.</p>
1492
  <a href="{{ url_for('join_as_guest', token=token) }}">Войти в комнату как гость</a>
1493
+ <p style="margin-top: 20px;"><a href="{{ url_for('index') }}">Или войти/зарегистрироваться</a></p>
1494
  </div>
1495
  </body>
1496
  </html>
1497
  ''', token=token)
1498
 
1499
+
1500
+ @socketio.on('connect')
1501
+ def handle_connect():
1502
+ print(f"Client connected: {request.sid}")
1503
+
1504
+ @socketio.on('disconnect')
1505
+ def handle_disconnect_custom():
1506
+ print(f"Client disconnected: {request.sid}")
1507
+
1508
  @socketio.on('join')
1509
  def handle_join(data):
1510
  token = data['token']
1511
  username = data['username']
1512
+ join_room(token)
1513
+
1514
+ if token not in rooms:
1515
+ return
1516
 
1517
+ all_users = rooms[token]['users'] + rooms[token]['guests']
1518
+ emit('init_users', {'users': rooms[token]['users'], 'guests': rooms[token]['guests']}, to=request.sid)
 
 
 
 
 
 
 
 
1519
 
1520
+ emit('user_joined', {
1521
+ 'username': username,
1522
+ 'users': rooms[token]['users'],
1523
+ 'guests': rooms[token]['guests']
1524
+ }, room=token, include_self=False)
1525
+
1526
+ if rooms[token]['current_game']:
1527
+ game_id = rooms[token]['current_game']
1528
+ emit('game_started', {'game_id': game_id}, to=request.sid)
1529
+ if token in games_data.get(game_id, {}).get('state', {}):
1530
+ emit('update_game_state', {'game_id': game_id, 'state': games_data[game_id]['state'][token]}, to=request.sid)
1531
 
1532
  @socketio.on('leave')
1533
  def handle_leave(data):
1534
  token = data['token']
1535
  username = data['username']
1536
  is_guest = data.get('is_guest', False)
1537
+ leave_room(token)
1538
 
1539
  if token in rooms:
1540
+ if is_guest:
1541
+ if username in rooms[token]['guests']:
1542
+ rooms[token]['guests'].remove(username)
1543
+ else:
1544
+ if username in rooms[token]['users']:
1545
+ rooms[token]['users'].remove(username)
1546
 
 
 
 
 
 
1547
  if not rooms[token]['users'] and not rooms[token]['guests']:
1548
  del rooms[token]
1549
  else:
1550
+ emit('user_left', {'username': username}, room=token)
 
1551
 
1552
  save_json(ROOMS_DB, rooms)
 
1553
 
1554
+ @socketio.on('player_move')
1555
+ def handle_player_move(data):
1556
  token = data['token']
1557
+ if token in rooms:
 
 
 
 
 
1558
  emit('player_moved', {
1559
+ 'username': data['username'],
1560
+ 'pos': data['pos'],
1561
+ 'quat': data['quat']
1562
  }, room=token, include_self=False)
1563
 
1564
  @socketio.on('signal')
1565
  def handle_signal(data):
1566
+ if data['token'] in rooms:
1567
+ emit('signal', {
1568
+ 'from': data['from'],
1569
+ 'signal': data['signal']
1570
+ }, room=data['to'])
1571
 
1572
+
1573
+ @socketio.on('start_game')
1574
+ def handle_start_game(data):
1575
  token = data['token']
1576
+ game_id = data['game_id']
1577
  username = session.get('username')
1578
 
1579
  if token in rooms and rooms[token].get('admin') == username:
1580
+ all_users_in_room = rooms[token]['users'] + rooms[token]['guests']
1581
+ min_p = games_data[game_id]['min_players']
1582
+ max_p = games_data[game_id]['max_players']
1583
+
1584
+ if not (min_p <= len(all_users_in_room) <= max_p):
1585
+ return
1586
 
1587
+ rooms[token]['current_game'] = game_id
1588
+ if game_id not in games_data: games_data[game_id] = {'state': {}}
1589
+ elif 'state' not in games_data[game_id]: games_data[game_id]['state'] = {}
1590
+
1591
+ games_data[game_id]['state'][token] = {}
 
 
 
1592
  save_json(ROOMS_DB, rooms)
1593
+ save_json(GAMES_DB, games_data)
1594
+ emit('game_started', {'game_id': game_id}, room=token)
1595
+
1596
 
1597
+ @socketio.on('set_game_state')
1598
+ def handle_set_game_state(data):
1599
  token = data['token']
1600
+ game_id = data['game_id']
1601
+ state = data['state']
1602
+ username = session.get('username')
1603
+
1604
+ if token in rooms and rooms[token].get('admin') == username:
1605
+ if rooms[token]['current_game'] == game_id:
1606
+ all_users = rooms[token]['users'] + rooms[token]['guests']
1607
+ state['players'] = all_users
1608
+
1609
+ if (game_id == 'crocodile' or game_id == 'alias') and 'presenter' not in state:
1610
+ state['presenter'] = random.choice(all_users) if all_users else None
1611
+
1612
+ if game_id == 'mafia' and not state.get('roles'):
1613
+ state['roles'] = assign_mafia_roles(all_users)
1614
+
1615
+ games_data[game_id]['state'][token] = state
1616
+ save_json(GAMES_DB, games_data)
1617
+ emit('update_game_state', {'game_id': game_id, 'state': state}, room=token)
1618
+
1619
+ if state.get('isRunning') and (game_id == 'crocodile' or game_id == 'alias'):
1620
+ start_timer(token, game_id)
1621
+
1622
+ def assign_mafia_roles(users):
1623
+ num_players = len(users)
1624
+ num_mafia = 1 if num_players < 6 else 2
1625
+ roles_list = ['mafia'] * num_mafia + ['civilian'] * (num_players - num_mafia)
1626
+ random.shuffle(roles_list)
1627
+ return {user: role for user, role in zip(users, roles_list)}
1628
+
1629
+ @socketio.on('game_action')
1630
+ def handle_game_action(data):
1631
+ token = data['token']
1632
+ game_id = data['game_id']
1633
+ action = data['action']
1634
+ user = data['user']
1635
+ value = data.get('value')
1636
+
1637
+ if token not in rooms or game_id != rooms[token].get('current_game'): return
1638
+ current_state = games_data[game_id]['state'].get(token, {})
1639
+ if not current_state.get('isRunning'): return
1640
+
1641
+ if game_id in ['crocodile', 'alias'] and action == 'guess':
1642
+ if user == current_state.get('presenter'): return
1643
+ result = "Угадано!" if value.lower() == current_state.get('word', '').lower() else "Не угадано"
1644
+ current_state.setdefault('guesses', []).append({'user': user, 'value': value, 'result': result})
1645
+ if result == "Угадано!": current_state['isRunning'] = False
1646
+
1647
+ elif game_id == 'mafia' and action == 'vote':
1648
+ if current_state['phase'] == 'day':
1649
+ current_state.setdefault('votes', {})[user] = value
1650
+ all_players = current_state.get('players', [])
1651
+ if len(current_state['votes']) == len(all_players):
1652
+ process_mafia_votes(current_state)
1653
+
1654
+ games_data[game_id]['state'][token] = current_state
1655
+ save_json(GAMES_DB, games_data)
1656
+ emit('update_game_state', {'game_id': game_id, 'state': current_state}, room=token)
1657
+
1658
+ def process_mafia_votes(state):
1659
+ votes = state.get('votes', {})
1660
+ counts = {}
1661
+ for vote in votes.values(): counts[vote] = counts.get(vote, 0) + 1
1662
+
1663
+ max_votes = 0
1664
+ voted_out = []
1665
+ for player, num_votes in counts.items():
1666
+ if num_votes > max_votes:
1667
+ max_votes = num_votes
1668
+ voted_out = [player]
1669
+ elif num_votes == max_votes:
1670
+ voted_out.append(player)
1671
+
1672
+ if len(voted_out) == 1:
1673
+ player_to_remove = voted_out[0]
1674
+ state['players'].remove(player_to_remove)
1675
+ state['killed'] = player_to_remove
1676
 
1677
+ remaining_mafia = sum(1 for p in state['players'] if state['roles'].get(p) == 'mafia')
1678
+ remaining_civilians = len(state['players']) - remaining_mafia
1679
 
1680
+ if remaining_mafia == 0:
1681
+ state['winner'] = 'Мирные'
1682
+ state['isRunning'] = False
1683
+ elif remaining_mafia >= remaining_civilians:
1684
+ state['winner'] = 'Мафия'
1685
+ state['isRunning'] = False
1686
+
1687
+ state['phase'] = 'night'
1688
+ state['votes'] = {}
1689
+
1690
+
1691
+ def start_timer(token, game_id):
1692
+ def timer_loop():
1693
+ with app.app_context():
1694
+ while True:
1695
+ state = games_data[game_id]['state'].get(token)
1696
+ if not state or not state.get('isRunning') or state.get('timer', 0) <= 0:
1697
+ if state:
1698
+ state['isRunning'] = False
1699
+ games_data[game_id]['state'][token] = state
1700
+ save_json(GAMES_DB, games_data)
1701
+ socketio.emit('update_game_state', {'game_id': game_id, 'state': state}, room=token)
1702
+ break
1703
+
1704
+ state['timer'] -= 1
1705
+ games_data[game_id]['state'][token] = state
1706
+ save_json(GAMES_DB, games_data)
1707
+ socketio.emit('update_game_state', {'game_id': game_id, 'state': state}, room=token)
1708
+ socketio.sleep(1)
1709
+ socketio.start_background_task(timer_loop)
1710
 
1711
  if __name__ == '__main__':
1712
+ if not os.path.exists(os.path.join(app.root_path, 'rooms.json')):
1713
+ with open(os.path.join(app.root_path, 'rooms.json'), 'w') as f: json.dump({}, f)
1714
+ if not os.path.exists(os.path.join(app.root_path, 'users.json')):
1715
+ with open(os.path.join(app.root_path, 'users.json'), 'w') as f: json.dump({}, f)
1716
+ if not os.path.exists(os.path.join(app.root_path, 'games.json')):
1717
+ save_json(GAMES_DB, games_data)
1718
+
1719
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1720
+ #backup_thread.start()
1721
+
1722
+ # ВАЖНО: для работы WebRTC нужен HTTPS. Werkzeug создает самоподписанный сертификат.
1723
+ # Вам нужно будет разрешить его в браузере (на странице "Дополнительно" -> "Перейти на сайт (небезопасно)").
1724
+ # Для продакшена используйте полноценный веб-сервер (Nginx) с настоящими SSL-сертификатами.
1725
+ try:
1726
+ socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True, ssl_context='adhoc')
1727
+ except TypeError:
1728
+ socketio.run(app, host='0.0.0.0', port=7860, debug=True, allow_unsafe_werkzeug=True)