awacke1 commited on
Commit
8cdadb5
·
verified ·
1 Parent(s): 67fe658

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -123
app.py CHANGED
@@ -484,8 +484,9 @@ def log_performance_metrics():
484
  st.sidebar.write(f"**{operation}:** {duration:.2f}s ({percentage:.1f}%)")
485
 
486
  # -----------------------------------------------------------
487
- # Enhanced 3D Game HTML (With dynamic insertion of username and avatar)
488
  # -----------------------------------------------------------
 
489
  rocky_map_html = f"""
490
  <!DOCTYPE html>
491
  <html lang="en">
@@ -529,7 +530,7 @@ rocky_map_html = f"""
529
  <div class="leaderboard" id="leaderboard">Leaderboard</div>
530
  <div id="chatBox"></div>
531
  <div class="controls">
532
- <p>Controls: WASD/Arrows to move, Space to collect treasure</p>
533
  <p>Chat to add world features!</p>
534
  </div>
535
  </div>
@@ -542,35 +543,32 @@ rocky_map_html = f"""
542
  const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
543
  camera.position.set(0, 50, 50);
544
  camera.lookAt(0, 0, 0);
545
-
546
- const renderer = new THREE.WebGLRenderer({{ antialias: true }});
547
  renderer.setSize(800, 600);
548
  document.getElementById('gameContainer').appendChild(renderer.domElement);
549
 
 
550
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
551
  scene.add(ambientLight);
552
  const sunLight = new THREE.DirectionalLight(0xffddaa, 1);
553
  sunLight.position.set(50, 50, 50);
554
  sunLight.castShadow = true;
555
  scene.add(sunLight);
556
-
557
  const groundGeometry = new THREE.PlaneGeometry(100, 100);
558
- const groundMaterial = new THREE.MeshStandardMaterial({{ color: 0x228B22 }});
559
  const ground = new THREE.Mesh(groundGeometry, groundMaterial);
560
  ground.rotation.x = -Math.PI / 2;
561
  ground.receiveShadow = true;
562
  scene.add(ground);
563
 
564
- let players = {{}};
565
- const playerMeshes = {{}};
566
- let treasures = [];
567
- const treasureMeshes = {{}};
568
- let worldObjects = [];
569
- const worldObjectMeshes = {{}};
570
- let xPos = 0, zPos = 0, moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
571
- let score = 0, treasureCount = 0, lastActive = performance.now() / 1000;
572
 
573
- const shapeGeometries = {{
 
 
574
  "sphere": new THREE.SphereGeometry(1, 16, 16),
575
  "cube": new THREE.BoxGeometry(2, 2, 2),
576
  "cylinder": new THREE.CylinderGeometry(1, 1, 2, 16),
@@ -580,171 +578,182 @@ rocky_map_html = f"""
580
  "octahedron": new THREE.OctahedronGeometry(1),
581
  "tetrahedron": new THREE.TetrahedronGeometry(1),
582
  "icosahedron": new THREE.IcosahedronGeometry(1)
583
- }};
584
-
585
- const playerMaterial = new THREE.MeshPhongMaterial({{ color: {next(c["color"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)} }});
586
  const playerMesh = new THREE.Mesh(shapeGeometries["{next(c['shape'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}"], playerMaterial);
587
  playerMesh.position.set(xPos, 1, zPos);
588
  playerMesh.castShadow = true;
589
  scene.add(playerMesh);
590
- players[playerName] = {{ mesh: playerMesh, score: 0, treasures: 0 }};
 
591
 
592
- function updateTreasures(treasureData) {{
593
- treasureData.forEach(t => {{
594
- if (!treasureMeshes[t.id]) {{
595
- const treasure = new THREE.Mesh(
596
- new THREE.SphereGeometry(1, 8, 8),
597
- new THREE.MeshPhongMaterial({{ color: 0xffff00 }})
598
- );
599
- treasure.position.set(t.x, 1, t.z);
600
- treasure.castShadow = true;
601
- treasureMeshes[t.id] = treasure;
602
- treasures.push(treasure);
603
- scene.add(treasure);
604
- }} else {{
605
- treasureMeshes[t.id].position.set(t.x, 1, t.z);
606
- }}
607
- }});
608
- Object.keys(treasureMeshes).forEach(id => {{
609
- if (!treasureData.some(t => t.id === id)) {{
610
- scene.remove(treasureMeshes[id]);
611
- treasures = treasures.filter(t => t !== treasureMeshes[id]);
612
- delete treasureMeshes[id];
613
- }}
614
- }});
615
- }}
616
 
617
- function updateWorldObjects(objectData) {{
618
- objectData.forEach(obj => {{
619
- if (!worldObjectMeshes[obj.type + obj.x + obj.z]) {{
620
- const geometry = shapeGeometries[obj.shape] || new THREE.BoxGeometry(2, 2, 2);
621
- const material = new THREE.MeshPhongMaterial({{ color: obj.color }});
622
- const objMesh = new THREE.Mesh(geometry, material);
623
- objMesh.position.set(obj.x, 1, obj.z);
624
- objMesh.castShadow = true;
625
- worldObjectMeshes[obj.type + obj.x + obj.z] = objMesh;
626
- worldObjects.push(objMesh);
627
- scene.add(objMesh);
628
- }}
629
- }});
630
- }}
631
-
632
- document.addEventListener('keydown', (event) => {{
633
- switch (event.code) {{
634
  case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
635
  case 'ArrowRight': case 'KeyD': moveRight = true; break;
636
  case 'ArrowUp': case 'KeyW': moveUp = true; break;
637
  case 'ArrowDown': case 'KeyS': moveDown = true; break;
638
  case 'Space': collect = true; break;
639
- }}
640
  lastActive = performance.now() / 1000;
641
- }});
642
- document.addEventListener('keyup', (event) => {{
643
- switch (event.code) {{
644
  case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
645
  case 'ArrowRight': case 'KeyD': moveRight = false; break;
646
  case 'ArrowUp': case 'KeyW': moveUp = false; break;
647
  case 'ArrowDown': case 'KeyS': moveDown = false; break;
648
  case 'Space': collect = false; break;
649
- }}
650
- }});
651
-
652
- function updatePlayer(delta) {{
653
- const speed = 20;
654
- if (moveLeft && xPos > -40) xPos -= speed * delta;
655
- if (moveRight && xPos < 40) xPos += speed * delta;
656
- if (moveUp && zPos > -40) zPos -= speed * delta;
657
- if (moveDown && zPos < 40) zPos += speed * delta;
 
 
 
 
 
 
 
 
 
 
658
  playerMesh.position.set(xPos, 1, zPos);
659
- ws.send(`${{playerName}}|MOVE:${{xPos}}:${{zPos}}`);
660
-
661
- if (collect) {{
662
- for (let i = treasures.length - 1; i >= 0; i--) {{
663
- if (playerMesh.position.distanceTo(treasures[i].position) < 2) {{
664
  const id = Object.keys(treasureMeshes).find(key => treasureMeshes[key] === treasures[i]);
665
  scene.remove(treasures[i]);
666
  treasures.splice(i, 1);
667
  delete treasureMeshes[id];
668
  score += 50;
669
  treasureCount += 1;
670
- ws.send(`${{playerName}}|SCORE:${{score}}`);
671
- ws.send(`${{playerName}}|TREASURE:${{treasureCount}}`);
672
- }}
673
- }}
674
- }}
675
-
676
- camera.position.set(xPos, 50, zPos + 50);
677
- camera.lookAt(xPos, 0, zPos);
678
- const timeout = 60 - (performance.now() / 1000 - lastActive);
679
- document.getElementById('timeout').textContent = `Timeout: ${{Math.max(0, timeout.toFixed(0))}}s`;
680
- document.getElementById('score').textContent = `Score: $${{score}}`;
681
- document.getElementById('treasures').textContent = `Treasures: $${{treasureCount}}`;
682
- }}
683
 
684
- function updatePlayers(playerData) {{
685
- playerData.forEach(player => {{
686
- if (!playerMeshes[player.username]) {{
 
687
  const geometry = shapeGeometries[player.shape] || new THREE.BoxGeometry(2, 2, 2);
688
- const material = new THREE.MeshPhongMaterial({{ color: player.color }});
689
  const mesh = new THREE.Mesh(geometry, material);
690
  mesh.castShadow = true;
691
  scene.add(mesh);
692
  playerMeshes[player.username] = mesh;
693
- }}
694
  const mesh = playerMeshes[player.username];
695
  mesh.position.set(player.x, 1, player.z);
696
- players[player.username] = {{ mesh: mesh, score: player.score, treasures: player.treasures }};
697
- if (player.username === playerName) {{
698
  xPos = player.x;
699
  zPos = player.z;
700
  score = player.score;
701
  treasureCount = player.treasures;
702
  playerMesh.position.set(xPos, 1, zPos);
703
- }}
704
- }});
705
- document.getElementById('players').textContent = `Players: ${{Object.keys(playerMeshes).length}}`;
706
  const leaderboard = playerData.sort((a, b) => b.score - a.score)
707
- .map(p => `${{p.username}}: $${{p.score}}`)
708
  .join('<br>');
709
- document.getElementById('leaderboard').innerHTML = `Leaderboard<br>${{leaderboard}}`;
710
- }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
 
712
- ws.onmessage = function(event) {{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
713
  const data = event.data;
714
- if (data.startsWith('MAP_UPDATE:')) {{
715
  const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
716
  updatePlayers(playerData);
717
- }} else if (data.startsWith('CHAT_UPDATE:')) {{
718
  const chatData = JSON.parse(data.split('CHAT_UPDATE:')[1]);
719
  const chatBox = document.getElementById('chatBox');
720
- chatBox.innerHTML = chatData.map(line => `<p>${{line}}</p>`).join('');
721
  chatBox.scrollTop = chatBox.scrollHeight;
722
- }} else if (data.startsWith('GAME_STATE:')) {{
723
  const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
724
  updatePlayers(gameState.players);
725
  updateTreasures(gameState.treasures);
726
  updateWorldObjects(gameState.world_objects);
727
- }} else if (!data.startsWith('PRAIRIE_UPDATE:')) {{
728
  const [sender, message] = data.split('|');
729
  const chatBox = document.getElementById('chatBox');
730
- chatBox.innerHTML += `<p>${{sender}}: ${{message}}</p>`;
731
  chatBox.scrollTop = chatBox.scrollHeight;
732
- }}
733
- }};
734
 
735
  let lastTime = performance.now();
736
- function animate() {{
737
  requestAnimationFrame(animate);
738
  const currentTime = performance.now();
739
  const delta = (currentTime - lastTime) / 1000;
740
  lastTime = currentTime;
741
-
742
  updatePlayer(delta);
743
  treasures.forEach(t => t.rotation.y += delta);
744
  worldObjects.forEach(o => o.rotation.y += delta * 0.5);
745
  renderer.render(scene, camera);
746
- }}
747
-
748
  animate();
749
  </script>
750
  </body>
@@ -790,7 +799,6 @@ def main():
790
  asyncio.run(save_chat_entry(st.session_state.username, "mountains", st.session_state.tts_voice))
791
 
792
  left_col, right_col = st.columns([2, 1])
793
-
794
  with left_col:
795
  components.html(rocky_map_html, width=800, height=600)
796
  chat_content = asyncio.run(load_chat())
@@ -818,7 +826,6 @@ def main():
818
  play_and_download_audio(audio_file)
819
  st.session_state.last_activity = time.time()
820
  st.rerun()
821
-
822
  with right_col:
823
  st.subheader("🌾 Prairie Map")
824
  prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8, tiles="CartoDB Positron")
@@ -844,7 +851,6 @@ def main():
844
  popup=f"{player['username']} ({player['animal']})"
845
  ).add_to(prairie_map)
846
  folium_static(prairie_map, width=600, height=400)
847
-
848
  animal = st.selectbox("Choose Animal 🐾", ["prairie_dog", "deer", "sheep", "groundhog"])
849
  location = st.selectbox("Move to 📍", list(PRAIRIE_LOCATIONS.keys()))
850
  if st.button("Move 🚶"):
@@ -856,7 +862,6 @@ def main():
856
  elapsed = time.time() - st.session_state.last_update
857
  remaining = max(0, st.session_state.update_interval - elapsed)
858
  st.sidebar.markdown(f"⏳ Next Update in: {int(remaining)}s")
859
-
860
  if time.time() - st.session_state.last_activity > st.session_state.timeout:
861
  st.sidebar.warning("Timed out! Refreshing in 5 seconds... ⏰")
862
  time.sleep(5)
@@ -869,11 +874,9 @@ def main():
869
  st.session_state.last_update = time.time()
870
  st.session_state.game_state_timestamp = time.time()
871
  st.rerun()
872
-
873
  if not st.session_state.get('server_running', False):
874
  st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)
875
  st.session_state.server_task.start()
876
-
877
  display_file_history_in_sidebar()
878
  log_performance_metrics()
879
 
 
484
  st.sidebar.write(f"**{operation}:** {duration:.2f}s ({percentage:.1f}%)")
485
 
486
  # -----------------------------------------------------------
487
+ # Enhanced 3D Game HTML (With dynamic insertion of username and fluid movement)
488
  # -----------------------------------------------------------
489
+ # Note: Added velocity-based movement for smoother control.
490
  rocky_map_html = f"""
491
  <!DOCTYPE html>
492
  <html lang="en">
 
530
  <div class="leaderboard" id="leaderboard">Leaderboard</div>
531
  <div id="chatBox"></div>
532
  <div class="controls">
533
+ <p>Controls: Use WASD or Arrow keys for movement. Space to collect treasure.</p>
534
  <p>Chat to add world features!</p>
535
  </div>
536
  </div>
 
543
  const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
544
  camera.position.set(0, 50, 50);
545
  camera.lookAt(0, 0, 0);
546
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
 
547
  renderer.setSize(800, 600);
548
  document.getElementById('gameContainer').appendChild(renderer.domElement);
549
 
550
+ // Lighting & Ground
551
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
552
  scene.add(ambientLight);
553
  const sunLight = new THREE.DirectionalLight(0xffddaa, 1);
554
  sunLight.position.set(50, 50, 50);
555
  sunLight.castShadow = true;
556
  scene.add(sunLight);
 
557
  const groundGeometry = new THREE.PlaneGeometry(100, 100);
558
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
559
  const ground = new THREE.Mesh(groundGeometry, groundMaterial);
560
  ground.rotation.x = -Math.PI / 2;
561
  ground.receiveShadow = true;
562
  scene.add(ground);
563
 
564
+ // Fluid Movement Variables
565
+ let velocity = new THREE.Vector2(0, 0);
566
+ const acceleration = 50; // Increase for faster acceleration
567
+ const friction = 5; // Friction factor to smooth movement
 
 
 
 
568
 
569
+ // Player Setup
570
+ let xPos = 0, zPos = 0;
571
+ const shapeGeometries = {
572
  "sphere": new THREE.SphereGeometry(1, 16, 16),
573
  "cube": new THREE.BoxGeometry(2, 2, 2),
574
  "cylinder": new THREE.CylinderGeometry(1, 1, 2, 16),
 
578
  "octahedron": new THREE.OctahedronGeometry(1),
579
  "tetrahedron": new THREE.TetrahedronGeometry(1),
580
  "icosahedron": new THREE.IcosahedronGeometry(1)
581
+ };
582
+ const playerMaterial = new THREE.MeshPhongMaterial({ color: {next(c["color"] for c in EDGE_TTS_VOICES if c["name"] == st.session_state.username)} });
 
583
  const playerMesh = new THREE.Mesh(shapeGeometries["{next(c['shape'] for c in EDGE_TTS_VOICES if c['name'] == st.session_state.username)}"], playerMaterial);
584
  playerMesh.position.set(xPos, 1, zPos);
585
  playerMesh.castShadow = true;
586
  scene.add(playerMesh);
587
+ let score = 0, treasureCount = 0;
588
+ let lastActive = performance.now() / 1000;
589
 
590
+ // Key Flags
591
+ let moveLeft = false, moveRight = false, moveUp = false, moveDown = false, collect = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
 
593
+ document.addEventListener('keydown', (event) => {
594
+ switch (event.code) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
596
  case 'ArrowRight': case 'KeyD': moveRight = true; break;
597
  case 'ArrowUp': case 'KeyW': moveUp = true; break;
598
  case 'ArrowDown': case 'KeyS': moveDown = true; break;
599
  case 'Space': collect = true; break;
600
+ }
601
  lastActive = performance.now() / 1000;
602
+ });
603
+ document.addEventListener('keyup', (event) => {
604
+ switch (event.code) {
605
  case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
606
  case 'ArrowRight': case 'KeyD': moveRight = false; break;
607
  case 'ArrowUp': case 'KeyW': moveUp = false; break;
608
  case 'ArrowDown': case 'KeyS': moveDown = false; break;
609
  case 'Space': collect = false; break;
610
+ }
611
+ });
612
+
613
+ // Fluid Movement Update using velocity-based control
614
+ function updatePlayer(delta) {
615
+ // Accelerate based on key presses
616
+ if (moveLeft) velocity.x -= acceleration * delta;
617
+ if (moveRight) velocity.x += acceleration * delta;
618
+ if (moveUp) velocity.y -= acceleration * delta;
619
+ if (moveDown) velocity.y += acceleration * delta;
620
+ // Apply friction
621
+ velocity.x -= velocity.x * friction * delta;
622
+ velocity.y -= velocity.y * friction * delta;
623
+ // Update positions
624
+ xPos += velocity.x * delta;
625
+ zPos += velocity.y * delta;
626
+ // Clamp position boundaries
627
+ xPos = Math.max(-40, Math.min(40, xPos));
628
+ zPos = Math.max(-40, Math.min(40, zPos));
629
  playerMesh.position.set(xPos, 1, zPos);
630
+ ws.send(`${playerName}|MOVE:${xPos}:${zPos}`);
631
+ // Treasure collection
632
+ if (collect) {
633
+ for (let i = treasures.length - 1; i >= 0; i--) {
634
+ if (playerMesh.position.distanceTo(treasures[i].position) < 2) {
635
  const id = Object.keys(treasureMeshes).find(key => treasureMeshes[key] === treasures[i]);
636
  scene.remove(treasures[i]);
637
  treasures.splice(i, 1);
638
  delete treasureMeshes[id];
639
  score += 50;
640
  treasureCount += 1;
641
+ ws.send(`${playerName}|SCORE:${score}`);
642
+ ws.send(`${playerName}|TREASURE:${treasureCount}`);
643
+ }
644
+ }
645
+ }
646
+ // Update camera position for a dynamic view
647
+ camera.position.lerp(new THREE.Vector3(xPos, 50, zPos + 50), 0.1);
648
+ camera.lookAt(new THREE.Vector3(xPos, 0, zPos));
649
+ document.getElementById('timeout').textContent = `Timeout: ${Math.max(0, (60 - (performance.now()/1000 - lastActive)).toFixed(0))}s`;
650
+ document.getElementById('score').textContent = `Score: $${score}`;
651
+ document.getElementById('treasures').textContent = `Treasures: $${treasureCount}`;
652
+ }
 
653
 
654
+ // Update functions for other players, treasures, and world objects remain similar
655
+ function updatePlayers(playerData) {
656
+ playerData.forEach(player => {
657
+ if (!playerMeshes[player.username]) {
658
  const geometry = shapeGeometries[player.shape] || new THREE.BoxGeometry(2, 2, 2);
659
+ const material = new THREE.MeshPhongMaterial({ color: player.color });
660
  const mesh = new THREE.Mesh(geometry, material);
661
  mesh.castShadow = true;
662
  scene.add(mesh);
663
  playerMeshes[player.username] = mesh;
664
+ }
665
  const mesh = playerMeshes[player.username];
666
  mesh.position.set(player.x, 1, player.z);
667
+ players[player.username] = { mesh: mesh, score: player.score, treasures: player.treasures };
668
+ if (player.username === playerName) {
669
  xPos = player.x;
670
  zPos = player.z;
671
  score = player.score;
672
  treasureCount = player.treasures;
673
  playerMesh.position.set(xPos, 1, zPos);
674
+ }
675
+ });
676
+ document.getElementById('players').textContent = `Players: ${Object.keys(playerMeshes).length}`;
677
  const leaderboard = playerData.sort((a, b) => b.score - a.score)
678
+ .map(p => `${p.username}: $${p.score}`)
679
  .join('<br>');
680
+ document.getElementById('leaderboard').innerHTML = `Leaderboard<br>${leaderboard}`;
681
+ }
682
+
683
+ function updateTreasures(treasureData) {
684
+ treasureData.forEach(t => {
685
+ if (!treasureMeshes[t.id]) {
686
+ const treasure = new THREE.Mesh(
687
+ new THREE.SphereGeometry(1, 8, 8),
688
+ new THREE.MeshPhongMaterial({ color: 0xffff00 })
689
+ );
690
+ treasure.position.set(t.x, 1, t.z);
691
+ treasure.castShadow = true;
692
+ treasureMeshes[t.id] = treasure;
693
+ treasures.push(treasure);
694
+ scene.add(treasure);
695
+ } else {
696
+ treasureMeshes[t.id].position.set(t.x, 1, t.z);
697
+ }
698
+ });
699
+ Object.keys(treasureMeshes).forEach(id => {
700
+ if (!treasureData.some(t => t.id === id)) {
701
+ scene.remove(treasureMeshes[id]);
702
+ treasures = treasures.filter(t => t !== treasureMeshes[id]);
703
+ delete treasureMeshes[id];
704
+ }
705
+ });
706
+ }
707
 
708
+ function updateWorldObjects(objectData) {
709
+ objectData.forEach(obj => {
710
+ if (!worldObjectMeshes[obj.type + obj.x + obj.z]) {
711
+ const geometry = shapeGeometries[obj.shape] || new THREE.BoxGeometry(2, 2, 2);
712
+ const material = new THREE.MeshPhongMaterial({ color: obj.color });
713
+ const objMesh = new THREE.Mesh(geometry, material);
714
+ objMesh.position.set(obj.x, 1, obj.z);
715
+ objMesh.castShadow = true;
716
+ worldObjectMeshes[obj.type + obj.x + obj.z] = objMesh;
717
+ worldObjects.push(objMesh);
718
+ scene.add(objMesh);
719
+ }
720
+ });
721
+ }
722
+
723
+ ws.onmessage = function(event) {
724
  const data = event.data;
725
+ if (data.startsWith('MAP_UPDATE:')) {
726
  const playerData = JSON.parse(data.split('MAP_UPDATE:')[1]);
727
  updatePlayers(playerData);
728
+ } else if (data.startsWith('CHAT_UPDATE:')) {
729
  const chatData = JSON.parse(data.split('CHAT_UPDATE:')[1]);
730
  const chatBox = document.getElementById('chatBox');
731
+ chatBox.innerHTML = chatData.map(line => `<p>${line}</p>`).join('');
732
  chatBox.scrollTop = chatBox.scrollHeight;
733
+ } else if (data.startsWith('GAME_STATE:')) {
734
  const gameState = JSON.parse(data.split('GAME_STATE:')[1]);
735
  updatePlayers(gameState.players);
736
  updateTreasures(gameState.treasures);
737
  updateWorldObjects(gameState.world_objects);
738
+ } else if (!data.startsWith('PRAIRIE_UPDATE:')) {
739
  const [sender, message] = data.split('|');
740
  const chatBox = document.getElementById('chatBox');
741
+ chatBox.innerHTML += `<p>${sender}: ${message}</p>`;
742
  chatBox.scrollTop = chatBox.scrollHeight;
743
+ }
744
+ };
745
 
746
  let lastTime = performance.now();
747
+ function animate() {
748
  requestAnimationFrame(animate);
749
  const currentTime = performance.now();
750
  const delta = (currentTime - lastTime) / 1000;
751
  lastTime = currentTime;
 
752
  updatePlayer(delta);
753
  treasures.forEach(t => t.rotation.y += delta);
754
  worldObjects.forEach(o => o.rotation.y += delta * 0.5);
755
  renderer.render(scene, camera);
756
+ }
 
757
  animate();
758
  </script>
759
  </body>
 
799
  asyncio.run(save_chat_entry(st.session_state.username, "mountains", st.session_state.tts_voice))
800
 
801
  left_col, right_col = st.columns([2, 1])
 
802
  with left_col:
803
  components.html(rocky_map_html, width=800, height=600)
804
  chat_content = asyncio.run(load_chat())
 
826
  play_and_download_audio(audio_file)
827
  st.session_state.last_activity = time.time()
828
  st.rerun()
 
829
  with right_col:
830
  st.subheader("🌾 Prairie Map")
831
  prairie_map = folium.Map(location=[44.0, -103.0], zoom_start=8, tiles="CartoDB Positron")
 
851
  popup=f"{player['username']} ({player['animal']})"
852
  ).add_to(prairie_map)
853
  folium_static(prairie_map, width=600, height=400)
 
854
  animal = st.selectbox("Choose Animal 🐾", ["prairie_dog", "deer", "sheep", "groundhog"])
855
  location = st.selectbox("Move to 📍", list(PRAIRIE_LOCATIONS.keys()))
856
  if st.button("Move 🚶"):
 
862
  elapsed = time.time() - st.session_state.last_update
863
  remaining = max(0, st.session_state.update_interval - elapsed)
864
  st.sidebar.markdown(f"⏳ Next Update in: {int(remaining)}s")
 
865
  if time.time() - st.session_state.last_activity > st.session_state.timeout:
866
  st.sidebar.warning("Timed out! Refreshing in 5 seconds... ⏰")
867
  time.sleep(5)
 
874
  st.session_state.last_update = time.time()
875
  st.session_state.game_state_timestamp = time.time()
876
  st.rerun()
 
877
  if not st.session_state.get('server_running', False):
878
  st.session_state.server_task = threading.Thread(target=start_websocket_server, daemon=True)
879
  st.session_state.server_task.start()
 
880
  display_file_history_in_sidebar()
881
  log_performance_metrics()
882