From 51995c36956839da85ccaa981076bc8817fff40a Mon Sep 17 00:00:00 2001 From: Iprok Date: Thu, 4 Sep 2025 13:39:53 +0300 Subject: [PATCH] Tg with Notifications, bags fixxed --- GAME_IMPROVEMENTS_README.md | 134 ++++++++ TELEGRAM_IMPROVEMENTS_README.md | 164 ++++++++++ TELEGRAM_STATUS_README.md | 78 +++++ TESTING_INSTRUCTIONS.md | 60 ++++ daily_availability.py | 60 ++++ saves/game_time.json | 2 +- server.js | 152 ++++++++- server/organizations.js | 4 +- src/Game.js | 543 ++++++++++++++++++++++++++++++-- src/api/auth.js | 40 +++ test_telegram_fix.html | 217 +++++++++++++ test_telegram_improvements.html | 201 ++++++++++++ test_telegram_status.html | 154 +++++++++ 13 files changed, 1775 insertions(+), 34 deletions(-) create mode 100644 GAME_IMPROVEMENTS_README.md create mode 100644 TELEGRAM_IMPROVEMENTS_README.md create mode 100644 TELEGRAM_STATUS_README.md create mode 100644 TESTING_INSTRUCTIONS.md create mode 100644 daily_availability.py create mode 100644 test_telegram_fix.html create mode 100644 test_telegram_improvements.html create mode 100644 test_telegram_status.html diff --git a/GAME_IMPROVEMENTS_README.md b/GAME_IMPROVEMENTS_README.md new file mode 100644 index 0000000..6a44950 --- /dev/null +++ b/GAME_IMPROVEMENTS_README.md @@ -0,0 +1,134 @@ +# Улучшения игры - Полный список + +## 🆕 Новые функции + +### 1. Уведомления о сообщениях в Telegram +- **Описание**: Показываются уведомления о новых сообщениях, когда Telegram не открыт +- **Как работает**: + - WebSocket событие `newMessage` обрабатывается на клиенте + - Уведомление появляется в правом верхнем углу экрана + - Автоматически исчезает через 5 секунд + - Анимированное появление и исчезновение +- **Файлы**: `src/Game.js` - добавлена функция `showMessageNotification()` + +### 2. Уведомления о перезагрузке сервера +- **Описание**: Игроки получают предупреждение о перезагрузке сервера +- **Как работает**: + - Сервер отправляет событие `serverRestart` всем клиентам + - Показывается красное уведомление с обратным отсчетом + - Сервер корректно завершает работу через 5 секунд +- **Файлы**: + - `server.js` - добавлена функция `gracefulShutdown()` + - `src/Game.js` - добавлена функция `showServerRestartNotification()` + +## 🎮 Улучшения управления + +### 3. Исправление застревания в текстурах +- **Проблема**: Игрок застревал в текстурах и не мог выбраться +- **Решение**: + - Уменьшены размеры коллизионной коробки игрока (с 0.25 до 0.2) + - Добавлено скольжение вдоль стен при столкновении + - Увеличен зазор между игроком и объектами (с 0.01 до 0.05) + - Улучшена система определения направления скольжения +- **Файлы**: `src/Game.js` - улучшена функция `updateFirstPersonMovement()` + +### 4. Улучшенное управление камерой +- **Новые возможности**: + - `Ctrl + колесо` = вертикальный поворот камеры (как было) + - `Shift + Ctrl + колесо` = горизонтальный поворот камеры (±90 градусов) +- **Ограничения**: Горизонтальный поворот ограничен для предотвращения дезориентации +- **Файлы**: `src/Game.js` - улучшена функция `onMouseWheel()` + +### 5. Отключение браузерного масштабирования +- **Проблема**: Браузер масштабировал страницу при Ctrl + колесо +- **Решение**: Добавлены обработчики событий для предотвращения масштабирования +- **Файлы**: `src/Game.js` - добавлены обработчики `wheel` и `keydown` + +## 🚀 Оптимизация производительности + +### 6. Улучшенная система загрузки ресурсов +- **Проблема**: Панель загрузки блокировала управление игрой +- **Решение**: + - Панель показывается только при начальной загрузке или загрузке >5 ресурсов + - Малые загрузки происходят в фоне без блокировки + - Добавлен флаг `isInitialLoad` для контроля отображения +- **Файлы**: `src/Game.js` - улучшен `LoadingManager` + +## 🔧 Технические улучшения + +### 7. Graceful Shutdown сервера +- **Описание**: Сервер корректно завершает работу при получении сигналов +- **Сигналы**: `SIGTERM`, `SIGINT` (Ctrl+C) +- **Процесс**: + 1. Уведомление всех клиентов + 2. Ожидание 5 секунд + 3. Закрытие HTTP сервера + 4. Принудительное завершение через 10 секунд + +### 8. Улучшенная система коллизий +- **Алгоритм скольжения**: + - Определение ближайшего препятствия + - Вычисление направления скольжения + - Проверка возможности движения в направлении скольжения + - Применение скольжения с уменьшенной дистанцией + +## 📱 Пользовательский интерфейс + +### 9. Подсказки по управлению +- **Описание**: Автоматически показывается подсказка об управлении камерой +- **Время**: Появляется через 3 секунды после загрузки игры +- **Длительность**: Висит 10 секунд, затем плавно исчезает +- **Стиль**: Темная полупрозрачная панель с анимацией + +## 🧪 Тестирование + +### 10. Тестовые файлы +- `test_telegram_status.html` - тест API статуса пользователей +- `TELEGRAM_STATUS_README.md` - документация по системе статусов +- `TESTING_INSTRUCTIONS.md` - инструкции по тестированию +- `GAME_IMPROVEMENTS_README.md` - этот файл + +## 🚀 Как запустить + +1. **Запуск сервера**: + ```bash + node server.js + ``` + +2. **Тестирование уведомлений**: + - Откройте игру в браузере + - Войдите в систему + - Откройте Telegram и отправьте сообщение + - Проверьте уведомления + +3. **Тестирование перезагрузки**: + - Нажмите Ctrl+C в терминале сервера + - Проверьте уведомление о перезагрузке + +4. **Тестирование управления**: + - `Ctrl + колесо` = вертикальный поворот + - `Shift + Ctrl + колесо` = горизонтальный поворот + +## 🔍 Отладка + +- **Сервер**: Логи в консоли терминала +- **Клиент**: Логи в консоли браузера (F12) +- **WebSocket**: Проверка соединения в Network tab + +## 📋 Чек-лист тестирования + +- [ ] Уведомления о сообщениях работают +- [ ] Уведомления о перезагрузке сервера работают +- [ ] Игрок не застревает в текстурах +- [ ] Управление камерой работает корректно +- [ ] Браузерное масштабирование отключено +- [ ] Панель загрузки не блокирует игру +- [ ] Подсказки по управлению отображаются + +## 🎯 Следующие шаги + +1. **Кэширование**: Добавить кэширование статуса пользователей +2. **Группировка**: Группировать пользователей по статусу в Telegram +3. **Уведомления**: Push-уведомления для важных событий +4. **Статистика**: Время онлайн, активность пользователей +5. **Оптимизация**: Дальнейшее улучшение производительности diff --git a/TELEGRAM_IMPROVEMENTS_README.md b/TELEGRAM_IMPROVEMENTS_README.md new file mode 100644 index 0000000..02ff1a3 --- /dev/null +++ b/TELEGRAM_IMPROVEMENTS_README.md @@ -0,0 +1,164 @@ +# Улучшения Telegram в игре + +## Обзор + +Этот документ описывает улучшения, внесенные в приложение Telegram (Shipgram) в игре, включая систему уведомлений, индикаторы непрочитанных сообщений и исправление проблемы "неизвестного отправителя". + +## Основные улучшения + +### 1. Уведомления о новых сообщениях + +**Проблема**: При получении новых сообщений в Telegram не было уведомлений, если приложение не было открыто. + +**Решение**: Реализована система уведомлений, которая показывает красивые всплывающие уведомления в правом верхнем углу экрана. + +**Функциональность**: +- Уведомления появляются только когда Telegram не открыт +- Отображают имя отправителя и текст сообщения +- Автоматически исчезают через 5 секунд +- Красивый дизайн с градиентом и анимацией + +**Код**: `showMessageNotification(senderId, messageText)` в `src/Game.js` + +### 2. Исправление проблемы "неизвестного отправителя" + +**Проблема**: В уведомлениях отображалось "Неизвестный" вместо имени отправителя. + +**Решение**: Реализована система поиска информации об отправителе: +1. Сначала ищет в списке контактов +2. Если не найден, загружает информацию с сервера через новый API endpoint + +**Новые API endpoints**: +- `GET /api/users/:userId` - получение информации о пользователе по ID +- `GET /api/messages-read/:contactId` - получение количества непрочитанных сообщений + +**Код**: Обновленная функция `showMessageNotification` в `src/Game.js` + +### 3. Индикаторы непрочитанных сообщений + +**Проблема**: Не было визуального индикатора непрочитанных сообщений в списке контактов. + +**Решение**: Добавлены счетчики непрочитанных сообщений: +- Красные бейджи с количеством непрочитанных сообщений +- Имена контактов выделяются жирным шрифтом при наличии непрочитанных сообщений +- Автоматическое обновление счетчиков + +**Функциональность**: +- `updateUnreadCount(senderId)` - обновляет счетчик для конкретного отправителя +- Автоматическое обновление при получении новых сообщений +- Периодическое обновление каждые 30 секунд + +**Код**: Функция `updateUnreadCount` и обновленный UI в `src/Game.js` + +## Технические детали + +### Серверная часть (`server.js`) + +#### Новые API endpoints + +```javascript +// Получение информации о пользователе по ID +app.get('/api/users/:userId', authenticate, async (req, res) => { + // Возвращает: id, firstName, lastName, avatarURL, isOnline, lastSeen +}); + +// Получение количества непрочитанных сообщений +app.get('/api/messages-read/:contactId', authenticate, async (req, res) => { + // Возвращает: { unreadCount: number } +}); +``` + +#### Исправления для onlineUsers и lastSeenTimes + +- Заменены `onlineUsers.hasOwnProperty()` на `onlineUsers.has()` +- Заменены `lastSeenTimes[userId]` на `lastSeenTimes.get(userId)` +- Исправлен `console.log` для отображения ключей Map + +### Клиентская часть (`src/Game.js`) + +#### Новые функции + +```javascript +// Показ уведомлений о сообщениях +const showMessageNotification = async (senderId, messageText) => { + // Логика поиска имени отправителя и показа уведомления +}; + +// Обновление счетчика непрочитанных сообщений +const updateUnreadCount = async (senderId) => { + // Загрузка и обновление счетчика +}; +``` + +#### Обновленный UI + +- Счетчики непрочитанных сообщений (красные бейджи) +- Выделение имен контактов жирным шрифтом +- Улучшенная структура контактов + +### API функции (`src/api/auth.js`) + +```javascript +// Загрузка информации о пользователе по ID +export const loadUserInfo = async (userId, token) => { + // Запрос к /api/users/:userId +}; + +// Обновленная функция получения статуса пользователей +export const getUsersStatus = async (token) => { + // Улучшенная обработка ошибок +}; +``` + +## Тестирование + +### Тестовый файл + +Создан `test_telegram_improvements.html` для тестирования новых API endpoints: + +1. **Тест статуса пользователей** - `/api/users/status` +2. **Тест информации о пользователе** - `/api/users/:userId` +3. **Тест количества непрочитанных сообщений** - `/api/messages-read/:contactId` + +### Инструкции по тестированию + +1. Откройте `test_telegram_improvements.html` в браузере +2. Введите JWT токен пользователя +3. Протестируйте каждый endpoint +4. Проверьте корректность возвращаемых данных + +## Интеграция с существующим кодом + +### WebSocket события + +- `newMessage` - автоматически вызывает `updateUnreadCount` и `showMessageNotification` +- `userStatusChanged` - обновляет статус пользователей в реальном времени + +### Интервалы обновления + +- `statusInterval` - каждые 30 секунд обновляет статусы и счетчики непрочитанных сообщений + +### Состояние компонента + +- `telegramContacts` - расширен для хранения `unreadCount` +- Автоматическое обновление при изменении данных + +## Преимущества + +1. **Лучший UX**: Пользователи видят уведомления о новых сообщениях +2. **Информативность**: Четко видно, от кого пришло сообщение +3. **Отслеживание**: Легко понять, какие чаты требуют внимания +4. **Производительность**: Эффективное обновление данных через интервалы +5. **Надежность**: Обработка ошибок и fallback для неизвестных отправителей + +## Возможные улучшения в будущем + +1. **Звуковые уведомления** - добавление звуковых сигналов +2. **Настройки уведомлений** - возможность отключения для определенных контактов +3. **Push-уведомления** - интеграция с браузерными push-уведомлениями +4. **Группировка уведомлений** - объединение уведомлений от одного отправителя +5. **История уведомлений** - сохранение истории показанных уведомлений + +## Заключение + +Реализованные улучшения значительно повышают удобство использования Telegram в игре, решая основные проблемы с уведомлениями и отображением информации об отправителях. Система стала более информативной и удобной для пользователей. diff --git a/TELEGRAM_STATUS_README.md b/TELEGRAM_STATUS_README.md new file mode 100644 index 0000000..c437472 --- /dev/null +++ b/TELEGRAM_STATUS_README.md @@ -0,0 +1,78 @@ +# Система статуса пользователей для Telegram (Shipgram) + +## Описание + +Реализована система отображения статуса "online/offline" для пользователей в Telegram приложении (Shipgram) игрового телефона. Статус показывает, действительно ли игрок находится онлайн в игре. + +## Что реализовано + +### 1. Серверная часть (server.js) + +- **Отслеживание онлайн пользователей**: Переменная `onlineUsers` хранит ID пользователей, которые в данный момент подключены к WebSocket +- **Отслеживание времени последнего онлайн**: Переменная `lastSeenTimes` хранит время последнего подключения/отключения каждого пользователя +- **WebSocket события**: + - При подключении: `userStatusChanged` с `isOnline: true` + - При отключении: `userStatusChanged` с `isOnline: false` +- **API endpoint**: `/api/users/status` возвращает список пользователей с их статусом + +### 2. Клиентская часть (Game.js) + +- **Обновленный API вызов**: `loadTelegramContacts()` теперь использует `/api/users/status` вместо `/api/users` +- **WebSocket обработчик**: Слушает события `userStatusChanged` и обновляет статус в реальном времени +- **Периодическое обновление**: Каждые 30 секунд обновляет статус пользователей +- **Визуальные индикаторы**: + - Зеленая точка для онлайн пользователей + - Цветной текст статуса (зеленый для онлайн, серый для офлайн) + - Время последнего онлайн для офлайн пользователей + +### 3. API функции (auth.js) + +- Добавлена функция `getUsersStatus()` для получения статуса пользователей + +## Как это работает + +1. **Подключение пользователя**: + - Пользователь входит в игру + - WebSocket middleware добавляет его в `onlineUsers` + - Обновляется `lastSeenTimes` + - Отправляется событие `userStatusChanged` всем клиентам + +2. **Отключение пользователя**: + - Пользователь выходит из игры или теряет соединение + - Обновляется `lastSeenTimes` + - Отправляется событие `userStatusChanged` всем клиентам + - Пользователь удаляется из `onlineUsers` + +3. **Отображение в Telegram**: + - При открытии Telegram загружается список пользователей с их статусом + - Статус обновляется в реальном времени через WebSocket + - Периодически обновляется через API для синхронизации + +## Файлы, которые были изменены + +- `server.js` - добавлена логика отслеживания статуса и WebSocket события +- `src/Game.js` - обновлено Telegram приложение для отображения статуса +- `src/api/auth.js` - добавлена функция для получения статуса пользователей +- `test_telegram_status.html` - тестовый файл для проверки API + +## Тестирование + +1. Запустите сервер +2. Откройте `test_telegram_status.html` в браузере +3. Введите JWT токен пользователя +4. Нажмите "Тестировать API" +5. Проверьте, что статус пользователей отображается корректно + +## Особенности + +- **Реальное время**: Статус обновляется мгновенно при подключении/отключении +- **Надежность**: Периодическое обновление через API обеспечивает синхронизацию +- **Производительность**: WebSocket события отправляются только при изменении статуса +- **Визуальная обратная связь**: Зеленые точки и цветной текст для лучшего UX + +## Возможные улучшения + +1. **Кэширование**: Добавить кэширование статуса пользователей +2. **Группировка**: Группировать пользователей по статусу +3. **Уведомления**: Уведомления о том, что пользователь стал онлайн +4. **Статистика**: Время, проведенное онлайн, активность и т.д. diff --git a/TESTING_INSTRUCTIONS.md b/TESTING_INSTRUCTIONS.md new file mode 100644 index 0000000..291b48f --- /dev/null +++ b/TESTING_INSTRUCTIONS.md @@ -0,0 +1,60 @@ +# Инструкция по тестированию системы статуса пользователей + +## Быстрый тест + +1. **Запустите сервер**: + ```bash + node server.js + ``` + +2. **Откройте игру в браузере** и войдите в систему + +3. **Откройте Telegram в игровом телефоне** и проверьте: + - Отображается ли статус "Онлайн" для текущего пользователя + - Есть ли зеленая точка рядом с аватаром + - Показывает ли статус "Офлайн" для других пользователей + +## Детальное тестирование + +### Тест 1: Проверка API +1. Откройте `test_telegram_status.html` в браузере +2. Введите JWT токен из localStorage браузера +3. Нажмите "Тестировать API" +4. Проверьте, что возвращается список пользователей с полями: + - `isOnline`: boolean + - `lastSeen`: timestamp или null + +### Тест 2: Проверка WebSocket событий +1. Откройте консоль браузера +2. Войдите в игру +3. Проверьте логи: + ``` + Статус пользователя изменился: {userId: X, isOnline: true} + ``` + +### Тест 3: Проверка реального времени +1. Откройте игру в двух вкладках браузера +2. Войдите под разными пользователями +3. В одной вкладке откройте Telegram +4. В другой вкладке закройте игру +5. Проверьте, что статус изменился на "Офлайн" в реальном времени + +## Ожидаемые результаты + +- ✅ Статус "Онлайн" отображается только для пользователей, которые действительно в игре +- ✅ Зеленая точка появляется рядом с аватаром онлайн пользователей +- ✅ Статус обновляется в реальном времени при подключении/отключении +- ✅ Время последнего онлайн отображается для офлайн пользователей +- ✅ В консоли сервера видны логи подключения/отключения пользователей + +## Возможные проблемы + +1. **Статус не обновляется**: Проверьте WebSocket соединение +2. **API возвращает ошибку**: Проверьте JWT токен и права доступа +3. **Статус не синхронизируется**: Проверьте логи сервера на наличие ошибок + +## Отладка + +- **Сервер**: Смотрите логи в консоли сервера +- **Клиент**: Смотрите логи в консоли браузера +- **WebSocket**: Проверьте соединение в Network tab браузера diff --git a/daily_availability.py b/daily_availability.py new file mode 100644 index 0000000..bc74973 --- /dev/null +++ b/daily_availability.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# daily_availability.py + +import logging +from datetime import date +import httpx +from apscheduler.schedulers.blocking import BlockingScheduler +from apscheduler.triggers.cron import CronTrigger +from config import * + +# ====== CONFIGURATION ====== +API_BASE = "http://127.0.0.1:8000" +TIMEOUT = httpx.Timeout(connect=5.0, read=30.0, write=5.0, pool=5.0) + +# ====== LOGGING SETUP ====== +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s | %(levelname)s | %(message)s', + handlers=[logging.FileHandler("daily_availability.log", encoding="utf-8")] +) +logger = logging.getLogger(__name__) + +# ====== JOB FUNCTION ====== +def check_availability(): + today = date.today().isoformat() + url = f"{API_BASE}/availability_group" + headers = { + "X-API-KEY": API_KEY, + "Content-Type": "application/json" # хотя httpx сам проставит, но можно явно + } + payload = {"dates": [today]} + + logger.info(f"Запрос создания доступности групп на {today}") + try: + with httpx.Client(timeout=TIMEOUT) as client: + response = client.post(url, headers=headers, json=payload) + response.raise_for_status() + data = response.json() + logger.info(f"Успешно создана доступность: {data}") + except httpx.HTTPStatusError as e: + allow = e.response.headers.get("Allow") + logger.error(f"Метод не разрешён (status={e.response.status_code}), Allow={allow}") + except Exception as e: + logger.error(f"Ошибка при запросе доступности: {e}") + + +# ====== SCHEDULER SETUP ====== +if __name__ == "__main__": + scheduler = BlockingScheduler(timezone="Asia/Yerevan") + # Запускаем задачу каждый день в 00:01 + scheduler.add_job( + check_availability, + trigger=CronTrigger(hour=0, minute=1), + name="daily_availability_check" + ) + logger.info("Scheduler запущен. Ожидание выполнения задачи в 00:01...") + try: + scheduler.start() + except (KeyboardInterrupt, SystemExit): + logger.info("Scheduler остановлен пользователем") diff --git a/saves/game_time.json b/saves/game_time.json index d733d7f..14ae2d3 100644 --- a/saves/game_time.json +++ b/saves/game_time.json @@ -1 +1 @@ -{"time":"2025-04-23T00:30:00.608Z","lastReal":1756243565634} \ No newline at end of file +{"time":"2025-06-16T00:46:41.600Z","lastReal":1756826890758} \ No newline at end of file diff --git a/server.js b/server.js index 00ddff1..99c6a96 100644 --- a/server.js +++ b/server.js @@ -33,7 +33,7 @@ catch (e) { console.error('Ошибка при импорте db1 - virtual_World:', e); throw e; } - +/* try { new_quest_Base = require('./db2'); console.log('db2 - new_quest_Base - успешно импортирован'); @@ -42,7 +42,7 @@ catch (e) { console.error('Ошибка при импорте db2 - new_quest_Base: ', e); throw e; } - +*/ try { db = require('./db'); console.log('db успешно импортирован'); @@ -99,7 +99,8 @@ const io = require('socket.io')(http, { } }); -let onlineUsers = {}; +let onlineUsers = new Map(); +let lastSeenTimes = new Map(); // Добавляем отслеживание времени последнего онлайн const organizationsRouter = require('./server/organizations')(io, onlineUsers); app.use('/api/organizations', organizationsRouter); @@ -120,7 +121,19 @@ io.use((socket, next) => { try { const payload = jwt.verify(token, process.env.JWT_SECRET); socket.userId = payload.id; - onlineUsers[socket.userId] = socket.id; // Добавить пользователя в онлайн + onlineUsers.set(socket.userId, socket.id); // Добавить пользователя в онлайн + + // Обновляем время последнего онлайн + lastSeenTimes.set(socket.userId, new Date()); + console.log(`Пользователь ${socket.userId} стал онлайн, время: ${lastSeenTimes.get(socket.userId)}`); + + // Уведомляем всех клиентов о том, что пользователь стал онлайн + socket.broadcast.emit('userStatusChanged', { + userId: socket.userId, + isOnline: true + }); + console.log(`Отправлено событие userStatusChanged для пользователя ${socket.userId} (онлайн)`); + next(); } catch (err) { next(new Error('Invalid token')); @@ -315,7 +328,7 @@ io.on('connection', socket => { ); const newMessage = result.rows[0]; - const receiverSocketId = onlineUsers[recvId]; + const receiverSocketId = onlineUsers.get(recvId); // Отправка получателю if (receiverSocketId) { @@ -404,7 +417,18 @@ io.on('connection', socket => { // --- Отключение --- socket.on('disconnect', async () => { - delete onlineUsers[socket.userId]; + // Обновляем время последнего онлайн + lastSeenTimes.set(socket.userId, new Date()); + console.log(`Пользователь ${socket.userId} стал офлайн, время: ${lastSeenTimes.get(socket.userId)}`); + + // Уведомляем всех клиентов о том, что пользователь стал офлайн + socket.broadcast.emit('userStatusChanged', { + userId: socket.userId, + isOnline: false + }); + console.log(`Отправлено событие userStatusChanged для пользователя ${socket.userId} (офлайн)`); + + onlineUsers.delete(socket.userId); const cityId = socket.cityId; const player = playersByCity[cityId]?.[socket.id]; if (player) { @@ -439,6 +463,82 @@ app.get('/api/users', authenticate, async (req, res) => { } }); +// API endpoint для получения статуса пользователей для Telegram +app.get('/api/users/status', authenticate, async (req, res) => { + try { + console.log(`Запрос статуса пользователей от пользователя ${req.user.id}`); + + const { rows } = await db.query(` + SELECT id, first_name AS "firstName", last_name AS "lastName", avatar_url AS "avatarURL" + FROM users + WHERE id != $1 + `, [req.user.id]); + + // Добавляем статус online и время последнего онлайн для каждого пользователя + const usersWithStatus = rows.map(user => ({ + ...user, + isOnline: onlineUsers.has(user.id), + lastSeen: lastSeenTimes.get(user.id) || null + })); + + console.log(`Возвращено ${usersWithStatus.length} пользователей с статусом`); + console.log('Онлайн пользователи:', Array.from(onlineUsers.keys())); + + res.json(usersWithStatus); + } catch (e) { + console.error('Ошибка получения статуса пользователей', e); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// API endpoint to get user information by ID +app.get('/api/users/:userId', authenticate, async (req, res) => { + const userId = parseInt(req.params.userId, 10); + try { + const { rows } = await db.query(` + SELECT id, first_name AS "firstName", last_name AS "lastName", avatar_url AS "avatarURL" + FROM users + WHERE id = $1 + `, [userId]); + if (rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + const user = rows[0]; + const isOnline = onlineUsers.has(user.id); + const lastSeen = lastSeenTimes.get(user.id) || new Date(); + res.json({ + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + avatarURL: user.avatarURL, + isOnline: isOnline, + lastSeen: lastSeen + }); + } catch (e) { + console.error('Ошибка получения информации о пользователе по ID', e); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// API endpoint to get unread message count for a specific contact +app.get('/api/messages-read/:contactId', authenticate, async (req, res) => { + const userId = req.user.id; + const contactId = parseInt(req.params.contactId, 10); + + try { + const { rows } = await db.query(` + SELECT COUNT(*) as unread_count + FROM messages + WHERE sender_id = $1 AND recipient_id = $2 AND is_read = false + `, [contactId, userId]); + + res.json({ unreadCount: parseInt(rows[0].unread_count) }); + } catch (e) { + console.error('Ошибка получения количества непрочитанных сообщений:', e); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + // Новый маршрут для получения сообщений с конкретным контактом app.get('/api/messages/:contactId', authenticate, async (req, res) => { const userId = req.user.id; @@ -595,7 +695,7 @@ app.post('/api/messages/send', authenticate, async (req, res) => { ); const newMessage = result.rows[0]; - const receiverSocketId = onlineUsers[recvId]; + const receiverSocketId = onlineUsers.get(recvId); if (receiverSocketId) { io.to(receiverSocketId).emit('newMessage', { id: newMessage.id, @@ -1531,10 +1631,46 @@ app.use((req, res) => { }); const PORT = process.env.PORT || 4000; -http.listen(PORT, () => { +const server = http.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); +// Обработка сигналов для graceful shutdown +process.on('SIGTERM', () => { + console.log('SIGTERM received, shutting down gracefully...'); + gracefulShutdown(); +}); + +process.on('SIGINT', () => { + console.log('SIGINT received, shutting down gracefully...'); + gracefulShutdown(); +}); + +function gracefulShutdown() { + console.log('Уведомляем всех клиентов о перезагрузке сервера...'); + + // Отправляем уведомление всем подключенным клиентам + io.emit('serverRestart', { + message: 'Сервер будет перезагружен через 5 секунд. Пожалуйста, сохраните прогресс.', + restartIn: 5000 + }); + + // Даем время клиентам получить уведомление + setTimeout(() => { + console.log('Закрываем сервер...'); + server.close(() => { + console.log('HTTP server closed'); + process.exit(0); + }); + + // Принудительно закрываем через 10 секунд + setTimeout(() => { + console.error('Could not close connections in time, forcefully shutting down'); + process.exit(1); + }, 10000); + }, 5000); +} + // Логирование всех маршрутов и middleware ['get', 'post', 'put', 'delete', 'use'].forEach(method => { const orig = app[method]; diff --git a/server/organizations.js b/server/organizations.js index 3eeb5cd..39b5b6d 100644 --- a/server/organizations.js +++ b/server/organizations.js @@ -169,7 +169,7 @@ module.exports = function(io, onlineUsers) { let thirst = parseFloat(rows[0].thirst ?? 100); if (balance < price) { - const sock = onlineUsers[req.user.id]; + const sock = onlineUsers.get(req.user.id); if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вам недостаточно средств для покупки ${itemDef.name}` }); return res.status(400).json({ error: 'insufficient funds' }); } @@ -196,7 +196,7 @@ module.exports = function(io, onlineUsers) { satiety = Math.min(100, satiety + parseFloat(itemDef.hunger_gain)); thirst = Math.min(100, thirst + parseFloat(itemDef.thirst_gain)); - const sock = onlineUsers[req.user.id]; + const sock = onlineUsers.get(req.user.id); if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вы купили ${itemDef.name}` }); res.json({ success: true, balance, satiety, thirst }); diff --git a/src/Game.js b/src/Game.js index c14b02d..ade4600 100644 --- a/src/Game.js +++ b/src/Game.js @@ -15,6 +15,8 @@ import Inventory from './components/Inventory'; import OrgControlPanel from './components/OrgControlPanel'; import DoubleTapWrapper from './pages/DoubleTapWrapper'; import WaveformPlayer from './pages/WaveformPlayer'; +import { getUsersStatus, loadUserInfo } from './api/auth.js'; + function Game({ avatarUrl, gender }) { // 1) реф для хранилища сцены @@ -31,6 +33,7 @@ function Game({ avatarUrl, gender }) { const cleanupTimerRef = useRef(null); // Глобальный менеджер прогресса загрузки (используем в GLTFLoader) const loadingManagerRef = useRef(null); + const overlayTimeoutRef = useRef(null); // Кликабельные объекты внутри интерьера const interiorInteractablesRef = useRef([]); const npcMeshesRef = useRef([]); @@ -1341,14 +1344,19 @@ function Game({ avatarUrl, gender }) { const token = localStorage.getItem('token'); try { setTgError(null); - const res = await fetch('/api/users', { + const res = await fetch('/api/users/status', { headers: { Authorization: `Bearer ${token}` }, credentials: 'include', cache: 'no-cache' }); if (res.ok) { const data = await res.json(); - setTelegramContacts(data); + // Добавляем счетчик непрочитанных сообщений для каждого пользователя + const dataWithUnread = data.map(user => ({ + ...user, + unreadCount: 0 + })); + setTelegramContacts(dataWithUnread); } else { const txt = await res.text().catch(() => ''); console.error('Ошибка загрузки контактов Telegram', res.status, txt); @@ -1367,6 +1375,223 @@ function Game({ avatarUrl, gender }) { //const [readmes, setReadmes] = useState('false'); const [userProfile, setUserProfile] = useState(null); + // Функция показа уведомлений о сообщениях + const showMessageNotification = async (senderId, messageText) => { + try { + // Сначала пытаемся найти отправителя в контактах + let senderName = 'Неизвестный'; + const contact = telegramContacts.find(c => c.id === senderId); + + if (contact) { + senderName = contact.firstName || contact.lastName || 'Неизвестный'; + } else { + // Если не найден в контактах, загружаем информацию о пользователе + try { + const userInfo = await loadUserInfo(senderId, localStorage.getItem('token')); + senderName = userInfo.firstName || userInfo.lastName || 'Неизвестный'; + } catch (error) { + console.error('Ошибка загрузки информации о пользователе:', error); + senderName = 'Неизвестный'; + } + } + + // Создаем уведомление + const notification = document.createElement('div'); + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 15px 20px; + border-radius: 10px; + box-shadow: 0 8px 32px rgba(0,0,0,0.3); + z-index: 10000; + font-family: 'Arial', sans-serif; + font-size: 14px; + max-width: 300px; + transform: translateX(400px); + transition: transform 0.3s ease-out; + backdrop-filter: blur(10px); + border: 1px solid rgba(255,255,255,0.2); + `; + + notification.innerHTML = ` +
${senderName}
+
${messageText.length > 50 ? messageText.substring(0, 50) + '...' : messageText}
+ `; + + document.body.appendChild(notification); + + // Анимация появления + setTimeout(() => { + notification.style.transform = 'translateX(0)'; + }, 100); + + // Автоматическое скрытие через 5 секунд + setTimeout(() => { + notification.style.transform = 'translateX(400px)'; + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 300); + }, 5000); + + } catch (error) { + console.error('Ошибка показа уведомления:', error); + } + }; + + // Функция для обновления счетчика непрочитанных сообщений + const updateUnreadCount = async (senderId) => { + try { + const token = localStorage.getItem('token'); + if (!token) return; + + // Получаем количество непрочитанных сообщений + const response = await fetch(`/api/messages-read/${senderId}`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + const unreadCount = data.unreadCount || 0; + + // Обновляем счетчик в контактах + setTelegramContacts(prev => + prev.map(contact => + contact.id === senderId + ? { ...contact, unreadCount: unreadCount } + : contact + ) + ); + } + } catch (error) { + console.error('Ошибка обновления счетчика непрочитанных сообщений:', error); + } + }; + + // Функция показа подсказки об управлении камерой + function showCameraControlsHint() { + const hint = document.createElement('div'); + hint.style.cssText = ` + position: fixed; + bottom: 20px; + left: 20px; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 15px 20px; + border-radius: 10px; + font-family: system-ui, Arial, sans-serif; + font-size: 14px; + z-index: 9999; + max-width: 300px; + animation: fadeIn 0.5s ease-in; + `; + + hint.innerHTML = ` +
🎮 Управление камерой:
+
Ctrl + колесо = вертикальный поворот
+
Shift + Ctrl + колесо = горизонтальный поворот
+
Подсказка исчезнет через 10 секунд
+ `; + + // Добавляем CSS анимацию + const style = document.createElement('style'); + style.textContent = ` + @keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } + } + `; + document.head.appendChild(style); + + document.body.appendChild(hint); + + // Автоматически скрываем через 10 секунд + setTimeout(() => { + hint.style.animation = 'fadeOut 0.5s ease-out'; + hint.style.opacity = '0'; + setTimeout(() => hint.remove(), 500); + }, 10000); + + // Добавляем CSS для fadeOut + if (!document.querySelector('#hint-styles')) { + const fadeOutStyle = document.createElement('style'); + fadeOutStyle.id = 'hint-styles'; + fadeOutStyle.textContent = ` + @keyframes fadeOut { + from { opacity: 1; transform: translateY(0); } + to { opacity: 0; transform: translateY(20px); } + } + `; + document.head.appendChild(fadeOutStyle); + } + } + + // Функция показа уведомления о перезагрузке сервера + function showServerRestartNotification(message, restartIn) { + const notification = document.createElement('div'); + notification.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #dc2626; + color: white; + padding: 20px 30px; + border-radius: 15px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); + z-index: 10001; + max-width: 400px; + font-family: system-ui, Arial, sans-serif; + text-align: center; + animation: serverRestartPulse 2s infinite; + `; + + notification.innerHTML = ` +
⚠️ Перезагрузка сервера
+
${message}
+
Перезагрузка через: ${Math.ceil(restartIn/1000)} сек
+ `; + + // Добавляем CSS анимацию + const style = document.createElement('style'); + style.textContent = ` + @keyframes serverRestartPulse { + 0%, 100% { transform: translate(-50%, -50%) scale(1); } + 50% { transform: translate(-50%, -50%) scale(1.05); } + } + `; + document.head.appendChild(style); + + document.body.appendChild(notification); + + // Обновляем счетчик + const countdownEl = notification.querySelector('#restart-countdown'); + const startTime = Date.now(); + const countdownInterval = setInterval(() => { + const remaining = Math.max(0, restartIn - (Date.now() - startTime)); + if (countdownEl) { + countdownEl.textContent = Math.ceil(remaining/1000); + } + if (remaining <= 0) { + clearInterval(countdownInterval); + notification.remove(); + } + }, 100); + + // Автоматически скрываем через время перезагрузки + setTimeout(() => { + clearInterval(countdownInterval); + notification.remove(); + }, restartIn); + } + // Функция загрузки сообщений async function loadMessages(contactId) { if (!contactId) return; @@ -1910,11 +2135,18 @@ function Game({ avatarUrl, gender }) { } // ───────────────────────────────────────────── - // Красивый загрузочный оверлей + LoadingManager + // Улучшенный загрузочный оверлей + LoadingManager // ───────────────────────────────────────────── let overlayEl = null, barEl = null, textEl = null; + let isInitialLoad = true; // Флаг для определения начальной загрузки + function createLoadingOverlay() { if (overlayEl) return; + // Дополнительная проверка - не показываем overlay для очень маленьких загрузок + if (!isInitialLoad && loadingManagerRef.current && loadingManagerRef.current.itemStart) { + const currentTotal = loadingManagerRef.current.itemStart.length || 0; + if (currentTotal <= 3) return; // Не показываем для загрузки 3 или меньше ресурсов + } overlayEl = document.createElement('div'); Object.assign(overlayEl.style, { position: 'fixed', inset: '0', zIndex: 2000, @@ -1951,6 +2183,7 @@ function Game({ avatarUrl, gender }) { overlayEl.appendChild(pct); document.body.appendChild(overlayEl); } + function updateLoadingOverlay(percent, text) { if (!overlayEl) return; const p = Math.max(0, Math.min(100, Math.round(percent || 0))); @@ -1959,8 +2192,16 @@ function Game({ avatarUrl, gender }) { if (pct) pct.textContent = p + '%'; if (text && textEl) textEl.textContent = text; } + function removeLoadingOverlay() { if (!overlayEl) return; + + // Очищаем все таймеры overlay + if (overlayTimeoutRef.current) { + clearTimeout(overlayTimeoutRef.current); + overlayTimeoutRef.current = null; + } + overlayEl.style.transition = 'opacity .2s ease'; overlayEl.style.opacity = '0'; setTimeout(() => { @@ -1968,19 +2209,62 @@ function Game({ avatarUrl, gender }) { overlayEl = barEl = textEl = null; }, 220); } + // Общий менеджер загрузки (для GLTF/Texture и т.п.) const loadingManager = new THREE.LoadingManager(); loadingManagerRef.current = loadingManager; + loadingManager.onStart = (_url, loaded, total) => { - createLoadingOverlay(); - updateLoadingOverlay(total ? (loaded / total) * 100 : 5, 'Загрузка ресурсов...'); + console.log(`LoadingManager.onStart: isInitialLoad=${isInitialLoad}, total=${total}, url=${_url}`); + // Показываем оверлей только при начальной загрузке или при загрузке большого количества ресурсов + if (isInitialLoad || total > 10) { + console.log('Показываем overlay для загрузки'); + createLoadingOverlay(); + updateLoadingOverlay(total ? (loaded / total) * 100 : 5, 'Загрузка ресурсов...'); + } else { + console.log('Не показываем overlay - небольшая загрузка'); + } }; + loadingManager.onProgress = (_url, loaded, total) => { - updateLoadingOverlay(total ? (loaded / total) * 100 : 50); + if (overlayEl && (isInitialLoad || total > 10)) { + updateLoadingOverlay(total ? (loaded / total) * 100 : 50); + } }; + loadingManager.onLoad = () => { - updateLoadingOverlay(100, 'Инициализация сцены...'); - setTimeout(removeLoadingOverlay, 150); + console.log(`LoadingManager.onLoad: isInitialLoad=${isInitialLoad}, overlayEl=${!!overlayEl}`); + if (overlayEl) { + // Показываем "Инициализация сцены" только для начальной загрузки + if (isInitialLoad) { + console.log('Показываем "Инициализация сцены" для начальной загрузки'); + updateLoadingOverlay(100, 'Инициализация сцены...'); + setTimeout(removeLoadingOverlay, 150); + } else { + // Для небольших загрузок просто скрываем overlay + console.log('Скрываем overlay для небольшой загрузки'); + removeLoadingOverlay(); + } + } + isInitialLoad = false; // После первой загрузки сбрасываем флаг + + // Дополнительная защита - принудительно скрываем overlay через 3 секунды + if (overlayEl) { + overlayTimeoutRef.current = setTimeout(() => { + if (overlayEl) { + console.log('Принудительно скрываем overlay по таймауту'); + removeLoadingOverlay(); + } + }, 3000); + } + + // Глобальная защита - принудительно скрываем overlay через 5 секунд после начала игры + overlayTimeoutRef.current = setTimeout(() => { + if (overlayEl && !isInitialLoad) { + console.log('Глобальная защита: принудительно скрываем overlay'); + removeLoadingOverlay(); + } + }, 5000); }; @@ -1989,7 +2273,7 @@ function Game({ avatarUrl, gender }) { const baseOffset = new THREE.Vector3(-200, 150, -200); const planarDist = Math.hypot(baseOffset.x, baseOffset.z); const radius = Math.hypot(planarDist, baseOffset.y); - const baseAzimuth = Math.atan2(baseOffset.z, baseOffset.x); + let baseAzimuth = Math.atan2(baseOffset.z, baseOffset.x); const basePolar = Math.atan2(baseOffset.y, planarDist); let cameraPitchOffset = 0; @@ -2068,6 +2352,19 @@ function Game({ avatarUrl, gender }) { const p = JSON.parse(sessionStorage.getItem('user_profile') || '{}'); if (p?.id) socket.emit('economy:getBalance', { userId: p.id }); }, 3000); + + // Периодическое обновление статуса пользователей для Telegram + const statusInterval = setInterval(() => { + if (activeApp === "Telegram" && telegramContacts.length > 0) { + loadTelegramContacts(); + // Обновляем счетчики непрочитанных сообщений для всех контактов + telegramContacts.forEach(contact => { + if (contact.id !== profile.id) { + updateUnreadCount(contact.id); + } + }); + } + }, 30000); // Обновляем каждые 30 секунд socket.on('economy:balanceChanged', ({ userId, newBalance }) => { if (userId === profile.id) { setBalance(newBalance); @@ -2078,6 +2375,38 @@ function Game({ avatarUrl, gender }) { socket.emit('economy:getInventory', { userId: profile.id }); socket.on('economy:inventory', setInventory); socket.on('gameTime:update', ({ time }) => setGameTime(time)); + + // Обработчик изменения статуса пользователей для Telegram + socket.on('userStatusChanged', ({ userId, isOnline }) => { + console.log('Статус пользователя изменился:', { userId, isOnline }); + setTelegramContacts(prev => prev.map(user => + user.id === userId ? { ...user, isOnline } : user + )); + }); + + // Обработчик новых сообщений для уведомлений + socket.on('newMessage', ({ id, text, senderId, timestamp, isRead }) => { + console.log('Новое сообщение:', { id, text, senderId, timestamp, isRead }); + + // Показываем уведомление только если Telegram не открыт + if (activeApp !== "Telegram") { + showMessageNotification(senderId, text); + } + + // Обновляем счетчик непрочитанных сообщений + updateUnreadCount(senderId); + + // Обновляем список сообщений если открыт чат с этим пользователем + if (activeChat && activeChat.id === senderId) { + loadMessages(senderId); + } + }); + + // Обработчик перезагрузки сервера + socket.on('serverRestart', ({ message, restartIn }) => { + console.log('Сервер будет перезагружен:', { message, restartIn }); + showServerRestartNotification(message, restartIn); + }); // Лоадеры, учитывающиеся в прогрессе через loadingManagerRef const gltfLoader = new GLTFLoader(loadingManagerRef.current || undefined); const animLoader = new GLTFLoader(loadingManagerRef.current || undefined); @@ -2618,11 +2947,23 @@ function Game({ avatarUrl, gender }) { const delta = -e.deltaY * 0.001; if (e.ctrlKey) { + // При нажатом Ctrl управляем и вертикальным, и горизонтальным углом камеры + if (e.shiftKey) { + // Shift + Ctrl + колесо = горизонтальный поворот (влево-вправо) + const horizontalDelta = delta * 2; // Увеличиваем чувствительность + baseAzimuth = THREE.MathUtils.clamp( + baseAzimuth + horizontalDelta, + -Math.PI / 2, // -90 градусов + Math.PI / 2 // +90 градусов + ); + } else { + // Ctrl + колесо = вертикальный поворот (вверх-вниз) cameraPitchOffset = THREE.MathUtils.clamp( cameraPitchOffset + delta, -maxPitch, maxPitch ); + } } else { if (cameraRef.current === orthoCamRef.current) { zoom = THREE.MathUtils.clamp(zoom * (1 + delta), minZoom, maxZoom); @@ -3531,6 +3872,26 @@ function Game({ avatarUrl, gender }) { setShowInventory(v => !v); } + // Ctrl + Arrow keys for camera control + if (event.ctrlKey) { + const key = event.key.toLowerCase(); + if (key === 'arrowleft') { + const horizontalDelta = -0.1; // Поворот влево + baseAzimuth = THREE.MathUtils.clamp( + baseAzimuth + horizontalDelta, + -Math.PI / 2, // -90 градусов + Math.PI / 2 // +90 градусов + ); + } else if (key === 'arrowright') { + const horizontalDelta = 0.1; // Поворот вправо + baseAzimuth = THREE.MathUtils.clamp( + baseAzimuth + horizontalDelta, + -Math.PI / 2, // -90 градусов + Math.PI / 2 // +90 градусов + ); + } + } + // Сбрасываем назначение только если не в интерьере if (!isInInteriorRef.current) { destination = null; @@ -3846,6 +4207,7 @@ function Game({ avatarUrl, gender }) { // Поворот влево-вправо (A/D или стрелки) if (move.left) player.rotation.y += rotSpeed * delta; if (move.right) player.rotation.y -= rotSpeed * delta; + // Камера следует за вращением тела const headHeight = 1.6; const camBase = new THREE.Vector3(player.position.x, player.position.y + headHeight, player.position.z); @@ -3854,45 +4216,96 @@ function Game({ avatarUrl, gender }) { const lookForward = new THREE.Vector3(0, 0, -1).applyEuler(new THREE.Euler(0, player.rotation.y, 0)); fpCamRef.current.lookAt(fpCamRef.current.position.clone().add(lookForward)); - // Движение с проверкой коллизий + // Улучшенное движение с проверкой коллизий и предотвращением застревания const tryMove = (dirVec) => { - const candidate = player.position.clone().addScaledVector(dirVec, speed * delta); - // Обновляем AABB игрока (простая капсула не используется, только коробка) - const half = 0.25; // чуточку уже, чтобы не цепляться за стены - const height = 1.7; // немного ниже, чтобы не пересекать потолок + const stepDistance = speed * delta; + const candidate = player.position.clone().addScaledVector(dirVec, stepDistance); + + // Обновляем AABB игрока с меньшими размерами для предотвращения застревания + const half = 0.2; // Уменьшаем размер для лучшего прохождения + const height = 1.6; // Немного ниже для предотвращения застревания в потолке const playerBox = new THREE.Box3( new THREE.Vector3(candidate.x - half, candidate.y, candidate.z - half), new THREE.Vector3(candidate.x + half, candidate.y + height, candidate.z + half) ); + // Обновляем мировые матрицы статических коллайдеров для корректных AABB - try { interiorGroupRef.current && interiorGroupRef.current.updateMatrixWorld(true); } catch (_) { } + try { + interiorGroupRef.current && interiorGroupRef.current.updateMatrixWorld(true); + } catch (_) { } - // В интерьере учитываем только внутренние коллайдеры, без городских объектов + // В интерьере учитываем только внутренние коллайдеры const blockingMeshes = Array.isArray(interiorCollidersRef.current) ? interiorCollidersRef.current : []; let hits = false; + let closestDistance = Infinity; + let slideDirection = null; + for (const mesh of blockingMeshes) { if (!mesh) continue; const box = new THREE.Box3().setFromObject(mesh); - // небольшой зазор, чтобы скользить вдоль стен - const expanded = box.clone().expandByScalar(0.01); - if (expanded.intersectsBox(playerBox)) { hits = true; break; } + const expanded = box.clone().expandByScalar(0.05); // Увеличиваем зазор + + if (expanded.intersectsBox(playerBox)) { + hits = true; + + // Вычисляем направление скольжения вдоль стены + const center = box.getCenter(new THREE.Vector3()); + const toPlayer = player.position.clone().sub(center); + const distance = toPlayer.length(); + + if (distance < closestDistance) { + closestDistance = distance; + // Нормализуем и создаем направление скольжения + toPlayer.normalize(); + slideDirection = toPlayer; + } + } } + if (!hits) { + // Свободное движение player.position.copy(candidate); + } else if (slideDirection) { + // Скольжение вдоль стены + const slideDistance = stepDistance * 0.7; // Уменьшаем дистанцию скольжения + const slidePos = player.position.clone().addScaledVector(slideDirection, slideDistance); + + // Проверяем, можно ли двигаться в направлении скольжения + const slideBox = new THREE.Box3( + new THREE.Vector3(slidePos.x - half, slidePos.y, slidePos.z - half), + new THREE.Vector3(slidePos.x + half, slidePos.y + height, slidePos.z + half) + ); + + let canSlide = true; + for (const mesh of blockingMeshes) { + if (!mesh) continue; + const box = new THREE.Box3().setFromObject(mesh); + const expanded = box.clone().expandByScalar(0.05); + if (expanded.intersectsBox(slideBox)) { + canSlide = false; + break; + } + } + + if (canSlide) { + player.position.copy(slidePos); + } } }; const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(player.quaternion); const right = new THREE.Vector3(1, 0, 0).applyQuaternion(player.quaternion); + + // Применяем движение с плавностью if (move.forward) tryMove(forward); if (move.backward) tryMove(forward.clone().multiplyScalar(-1)); if (move.strafeLeft) tryMove(right.clone().multiplyScalar(-1)); if (move.strafeRight) tryMove(right); - // Отправляем позицию внутри интерьера, чтобы нас видели другие внутри + // Отправляем позицию внутри интерьера if (socketRef.current) { socketRef.current.emit('playerMovement', { x: player.position.x, y: player.position.y, z: player.position.z }); } @@ -4028,9 +4441,38 @@ function Game({ avatarUrl, gender }) { } } window.addEventListener('resize', onWindowResize, false); + + // Отключаем браузерное масштабирование + document.addEventListener('wheel', (e) => { + if (e.ctrlKey) { + e.preventDefault(); + } + }, { passive: false }); + + document.addEventListener('keydown', (e) => { + if (e.ctrlKey && (e.key === '+' || e.key === '-' || e.key === '=')) { + e.preventDefault(); + } + }); + + // Показываем подсказку об управлении камерой + setTimeout(() => { + showCameraControlsHint(); + }, 3000); return () => { clearInterval(balanceInterval); + clearInterval(statusInterval); + + // Очищаем overlay загрузки + if (overlayEl) { + removeLoadingOverlay(); + } + + // Очищаем все таймеры overlay + if (overlayTimeoutRef.current) { + clearTimeout(overlayTimeoutRef.current); + } // Очищаем таймеры throttling if (wheelTimeout) { @@ -5632,13 +6074,68 @@ function Game({ avatarUrl, gender }) { )} {telegramContacts.map((user) => (
setActiveChat(user)} style={{ padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', color: '#111' }}> +
{user.firstName?.[0]}{user.lastName?.[0]} +
+ {user.isOnline && ( +
+ )}
-
-
{user.firstName} {user.lastName}
-
Онлайн
+
+
0 ? 'bold' : 'normal' + }}> + {`${user.firstName || ''} ${user.lastName || ''}`} +
+
+ {user.isOnline ? 'Онлайн' : ( + user.lastSeen ? + `Был(а) ${new Date(user.lastSeen).toLocaleString('ru-RU', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })}` : + 'Офлайн' + )} +
+ {/* Счетчик непрочитанных сообщений */} + {user.unreadCount > 0 && ( +
+ {user.unreadCount > 99 ? '99+' : user.unreadCount} +
+ )}
))}
diff --git a/src/api/auth.js b/src/api/auth.js index a720bcd..9bcb3f6 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -33,4 +33,44 @@ export async function registerStep1(data) { }); return r.json(); } + +export const getUsersStatus = async (token) => { + try { + const response = await fetch('/api/users/status', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Failed to fetch users status'); + } + + return await response.json(); + } catch (error) { + console.error('Error fetching users status:', error); + throw error; + } +}; + +export const loadUserInfo = async (userId, token) => { + try { + const response = await fetch(`/api/users/${userId}`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Failed to fetch user info'); + } + + return await response.json(); + } catch (error) { + console.error('Error fetching user info:', error); + throw error; + } +}; \ No newline at end of file diff --git a/test_telegram_fix.html b/test_telegram_fix.html new file mode 100644 index 0000000..d233d4a --- /dev/null +++ b/test_telegram_fix.html @@ -0,0 +1,217 @@ + + + + + + Тест исправления Telegram + + + +

Тест исправления Telegram контактов

+ +
+

1. Тест загрузки контактов

+
+ + +
+ +
+
+ +
+

2. Тест получения информации о пользователе

+
+ + +
+
+ + +
+ +
+
+ +
+

3. Тест количества непрочитанных сообщений

+
+ + +
+
+ + +
+ +
+
+ + + + diff --git a/test_telegram_improvements.html b/test_telegram_improvements.html new file mode 100644 index 0000000..8f19bed --- /dev/null +++ b/test_telegram_improvements.html @@ -0,0 +1,201 @@ + + + + + + Тест улучшений Telegram + + + +

Тест улучшений Telegram

+ +
+

1. Получение статуса пользователей

+
+ + +
+ + +
+ +
+

2. Получение информации о пользователе по ID

+
+ + +
+
+ + +
+ + +
+ +
+

3. Получение количества непрочитанных сообщений

+
+ + +
+
+ + +
+ + +
+ + + + diff --git a/test_telegram_status.html b/test_telegram_status.html new file mode 100644 index 0000000..37ab239 --- /dev/null +++ b/test_telegram_status.html @@ -0,0 +1,154 @@ + + + + + + Тест Telegram Status API + + + +

Тест Telegram Status API

+ +
+
+ +
+ + +
+ +
+ + + +