let dotenv, express, db, Economy, GameTime, pathLib, fs, virtualWorldPool, new_quest_Base; try { dotenv = require('dotenv').config(); console.log('dotenv успешно импортирован'); } catch (e) { console.error('Ошибка при импорте dotenv:', e); console.log('Продолжаем без .env файла'); } // Устанавливаем fallback значения для критических переменных окружения if (!process.env.JWT_SECRET) { process.env.JWT_SECRET = 'fallback-secret-key-for-development'; console.warn('JWT_SECRET не найден, используем fallback ключ (НЕ ДЛЯ ПРОДАКШЕНА!)'); } if (!process.env.DATABASE_URL) { process.env.DATABASE_URL = 'postgresql://postgres:password@localhost:5432/revproj'; console.warn('DATABASE_URL не найден, используем fallback НЕ ДЛЯ ПРОДАКШЕНА!'); } if (!process.env.DATABASE_QUEST_NEW_QUESTS) { process.env.DATABASE_QUEST_NEW_QUESTS = 'postgresql://postgres:password@localhost:5432/quest_system'; console.warn('DATABASE_QUEST_NEW_QUESTS не найден, используем fallback НЕ ДЛЯ ПРОДАКШЕНА!'); } try { express = require('express'); console.log('express успешно импортирован'); } catch (e) { console.error('Ошибка при импорте express:', e); throw e; } try { virtualWorldPool = require('./db1'); console.log('db1 - virtualWorld - успешно импротирован'); } catch (e) { console.error('Ошибка при импорте db1 - virtual_World:', e); throw e; } try { new_quest_Base = require('./db2'); console.log('db2 - new_quest_Base - успешно импортирован'); } catch (e) { console.error('Ошибка при импорте db2 - new_quest_Base: ', e); throw e; } try { db = require('./db'); console.log('db успешно импортирован'); } catch (e) { console.error('Ошибка при импорте db:', e); throw e; } try { Economy = require('./economy'); console.log('Economy успешно импортирован'); } catch (e) { console.error('Ошибка при импорте economy:', e); throw e; } try { GameTime = require('./gameTime'); console.log('GameTime успешно импортирован'); } catch (e) { console.error('Ошибка при импорте gameTime:', e); throw e; } try { pathLib = require('path'); console.log('path успешно импортирован'); } catch (e) { console.error('Ошибка при импорте path:', e); throw e; } try { fs = require('fs'); console.log('fs успешно импортирован'); } catch (e) { console.error('Ошибка при импорте fs:', e); throw e; } const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); const http = require('http').createServer(app); const io = require('socket.io')(http, { cors: { origin: [ 'http://localhost:4000', 'http://rltn.online', 'https://rltn.online', 'http://www.rltn.online', 'https://www.rltn.online' ], methods: ['GET', 'POST'] } }); let onlineUsers = new Map(); let lastSeenTimes = new Map(); // Добавляем отслеживание времени последнего онлайн const organizationsRouter = require('./server/organizations')(io, onlineUsers); app.use('/api/organizations', organizationsRouter); // Инициализация игрового времени (ускорение 8x) и вещание клиентам try { if (GameTime && typeof GameTime === 'function') { global.__gameTimeInstance = global.__gameTimeInstance || new GameTime(io, 8); console.log('GameTime таймер запущен'); } } catch (e) { console.error('GameTime не запущен:', e); } io.use((socket, next) => { const token = socket.handshake.auth.token; if (!token) return next(new Error('No token')); try { const payload = jwt.verify(token, process.env.JWT_SECRET); socket.userId = payload.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')); } }); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); function authenticate(req, res, next) { const auth = req.headers.authorization?.split(' '); console.log('Проверка авторизации:', { hasAuth: !!auth, authType: auth?.[0], path: req.path }); try { if (!auth || auth[0] !== 'Bearer') { console.log('Ошибка: нет токена или неправильный формат'); return res.status(401).send('No token'); } const payload = jwt.verify(auth[1], process.env.JWT_SECRET); req.user = payload; console.log('Токен валиден, пользователь:', payload.id); next(); } catch (error) { console.log('Ошибка валидации токена:', error.message); res.status(401).send('Invalid token'); } } app.use(express.static(pathLib.join(__dirname, 'build'))); app.use( '/models', express.static(pathLib.join(__dirname, 'public', 'models')) ); let players = {}; // --- Расширяем players для поддержки городов --- let playersByCity = {}; io.on('connection', socket => { console.log('Player connected:', socket.id); // Получаем город игрока из БД (async () => { const { rows } = await db.query('SELECT last_city_id, last_pos_x, last_pos_z FROM users WHERE id = $1', [socket.userId]); const cityId = rows[0]?.last_city_id || 1; const x = rows[0]?.last_pos_x || 0; const z = rows[0]?.last_pos_z || 0; if (!playersByCity[cityId]) playersByCity[cityId] = {}; playersByCity[cityId][socket.id] = { socketId: socket.id, userId: socket.userId, x, y: 0, z, cityId, avatarURL: null, gender: null, firstName: null, lastName: null }; players[socket.id] = playersByCity[cityId][socket.id]; socket.cityId = cityId; socket.x = x; socket.y = 0; socket.z = z; // Отправляем только игроков этого города socket.emit('currentPlayers', playersByCity[cityId]); })(); // --- Новый игрок --- socket.on('newPlayer', data => { const cityId = socket.cityId || 1; if (!playersByCity[cityId]) playersByCity[cityId] = {}; const p = playersByCity[cityId][socket.id] || {}; Object.assign(p, { x: data.x, y: data.y ?? p.y ?? 0, z: data.z, cityId, avatarURL: data.avatarURL || null, gender: data.gender || null, firstName: data.firstName || '', lastName: data.lastName || '' }); playersByCity[cityId][socket.id] = p; players[socket.id] = p; socket.cityId = cityId; // Сообщаем всем игрокам этого города (без учёта интерьера) for (const id in playersByCity[cityId]) { if (id === socket.id) continue; io.to(id).emit('newPlayer', { playerId: socket.id, x: p.x, y: p.y ?? 0, z: p.z, avatarURL: p.avatarURL, gender: p.gender, firstName: p.firstName, lastName: p.lastName }); } }); // --- Перемещение игрока --- socket.on('playerMovement', movementData => { const cityId = socket.cityId; if (playersByCity[cityId] && playersByCity[cityId][socket.id]) { playersByCity[cityId][socket.id].x = movementData.x; if (typeof movementData.y === 'number') { playersByCity[cityId][socket.id].y = movementData.y; } playersByCity[cityId][socket.id].z = movementData.z; // Сообщаем только игрокам этого города (без учёта интерьера) for (const id in playersByCity[cityId]) { if (id !== socket.id) { io.to(id).emit('playerMoved', { playerId: socket.id, x: movementData.x, y: typeof movementData.y === 'number' ? movementData.y : (playersByCity[cityId][socket.id].y ?? 0), z: movementData.z }); } } // Voice chat nearby только в этом городе const norm = v => (v == null ? null : String(v)); const sender = playersByCity[cityId][socket.id]; for (const [id, other] of Object.entries(playersByCity[cityId])) { if (id === socket.id) continue; // Голосовой чат больше не фильтруем по интерьеру const dx = sender.x - other.x; const dz = sender.z - other.z; const dist = Math.sqrt(dx * dx + dz * dz); if (dist <= 50) { io.to(id).emit('voiceChatNearby', { playerId: socket.id }); io.to(socket.id).emit('voiceChatNearby', { playerId: id }); } } } }); // --- Смена интерьера (вход/выход) --- socket.on('interiorChange', ({ interiorId }) => { const cityId = socket.cityId; if (!playersByCity[cityId] || !playersByCity[cityId][socket.id]) return; const norm = v => (v == null ? null : String(v)); const prevInterior = playersByCity[cityId][socket.id].interiorId ?? null; const nextInterior = norm(interiorId); playersByCity[cityId][socket.id].interiorId = nextInterior; socket.interiorId = nextInterior; // Сообщаем игрокам старого интерьера, что этот игрок ушёл for (const id in playersByCity[cityId]) { if (id === socket.id) continue; const other = playersByCity[cityId][id]; const samePrev = norm(other?.interiorId) === norm(prevInterior); if (samePrev && prevInterior !== nextInterior) { io.to(id).emit('playerDisconnected', socket.id); } } // Сообщаем всем игрокам города о появлении (без учёта интерьера) for (const id in playersByCity[cityId]) { if (id === socket.id) continue; const p = playersByCity[cityId][socket.id]; io.to(id).emit('newPlayer', { playerId: socket.id, x: p.x, z: p.z, avatarURL: p.avatarURL, gender: p.gender, firstName: p.firstName, lastName: p.lastName }); } // Отправляем текущий список видимых игроков только для этого сокета // Отправляем полный список игроков города socket.emit('currentPlayers', playersByCity[cityId]); }); socket.on('sendMessage', async ({ receiverId, message }, callback) => { try { const senderId = socket.userId; const recvId = parseInt(receiverId, 10); // Проверка получателя const receiverCheck = await db.query('SELECT id FROM users WHERE id = $1', [recvId]); if (receiverCheck.rows.length === 0) { return callback({ error: 'Пользователь не найден' }); } // Сохранение сообщения const result = await virtualWorldPool.query( `INSERT INTO messages (sender_id, receiver_id, message) VALUES ($1, $2, $3) RETURNING id, created_at, is_read`, [senderId, recvId, message] ); const newMessage = result.rows[0]; const receiverSocketId = onlineUsers.get(recvId); // Отправка получателю if (receiverSocketId) { io.to(receiverSocketId).emit('newMessage', { id: newMessage.id, text: message, senderId, timestamp: newMessage.created_at, isRead: newMessage.is_read }); } callback({ success: true, message: newMessage }); } catch (err) { callback({ error: 'Ошибка отправки сообщения' }); } }); // --- Чат --- socket.on('chatMessage', ({ message, name }) => { const cityId = socket.cityId; const sender = playersByCity[cityId]?.[socket.id]; if (!sender) return; for (const [id, other] of Object.entries(playersByCity[cityId])) { const dx = sender.x - other.x; const dz = sender.z - other.z; const dist = Math.sqrt(dx * dx + dz * dz); if (dist <= 50 || id === socket.id) { io.to(id).emit('chatMessage', { playerId: socket.id, position: { x: sender.x, z: sender.z }, name: name || '???', message: message }); } } }); // --- WebRTC signaling --- socket.on('voiceChatOffer', ({ to, offer }) => { io.to(to).emit('voiceChatOffer', { from: socket.id, offer }); }); socket.on('voiceChatAnswer', ({ to, answer }) => { io.to(to).emit('voiceChatAnswer', { from: socket.id, answer }); }); socket.on('voiceChatIceCandidate', ({ to, candidate }) => { io.to(to).emit('voiceChatIceCandidate', { from: socket.id, candidate }); }); socket.on('voiceChatToggle', ({ enabled }) => { if (players[socket.id]) { players[socket.id].voiceEnabled = enabled; } socket.broadcast.emit('voiceChatStatus', { playerId: socket.id, enabled }); }); // --- Смена города --- socket.on('cityChange', async ({ cityId }) => { const oldCity = socket.cityId; if (playersByCity[oldCity]) { delete playersByCity[oldCity][socket.id]; // Сообщаем игрокам старого города о выходе for (const id in playersByCity[oldCity]) { io.to(id).emit('playerDisconnected', socket.id); } } if (!playersByCity[cityId]) playersByCity[cityId] = {}; playersByCity[cityId][socket.id] = { socketId: socket.id, userId: socket.userId, x: 0, z: 0, cityId, avatarURL: null, gender: null, firstName: null, lastName: null }; players[socket.id] = playersByCity[cityId][socket.id]; socket.cityId = cityId; // Отправляем новых игроков этого города socket.emit('currentPlayers', playersByCity[cityId]); }); // --- Отключение --- socket.on('disconnect', async () => { // Обновляем время последнего онлайн 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) { // Сохраняем координаты и город выхода await db.query( 'UPDATE users SET last_city_id = $1, last_pos_x = $2, last_pos_z = $3 WHERE id = $4', [cityId, player.x, player.z, player.userId] ); delete playersByCity[cityId][socket.id]; delete players[socket.id]; // Сообщаем игрокам города о выходе for (const id in playersByCity[cityId]) { io.to(id).emit('playerDisconnected', socket.id); } } }); }); // Маршрут для получения списка пользователей // Получить список пользователей (кроме текущего) app.get('/api/users', authenticate, async (req, res) => { 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 `, [req.user.id]); res.json(rows); } catch (e) { console.error('Ошибка получения списка пользователей', e); res.status(500).json({ error: 'Ошибка сервера' }); } }); // 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; const contactId = parseInt(req.params.contactId, 10); try { // Ensure table exists await virtualWorldPool.query(` CREATE TABLE IF NOT EXISTS messages ( id SERIAL PRIMARY KEY, sender_id INT NOT NULL, receiver_id INT NOT NULL, message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_read BOOLEAN NOT NULL DEFAULT FALSE )`); } catch (e) { console.warn('[GET /api/messages/:contactId] ensure table failed:', e.message); } try { const sql = `SELECT * FROM messages WHERE (sender_id = $1 AND receiver_id = $2) OR (sender_id = $2 AND receiver_id = $1) ORDER BY created_at ASC`; const messagesRes = await virtualWorldPool.query(sql, [userId, contactId]); res.json(messagesRes.rows); } catch (err) { console.error('[GET /api/messages/:contactId] error:', err); res.status(500).json({ error: 'Ошибка получения сообщений' }); } }); app.get('/api/messages/:contactId', authenticate, async (req, res) => { const userId = req.user.id; const contactId = parseInt(req.params.contactId, 10); try { // Ensure table exists await virtualWorldPool.query(` CREATE TABLE IF NOT EXISTS messages ( id SERIAL PRIMARY KEY, sender_id INT NOT NULL, receiver_id INT NOT NULL, message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_read BOOLEAN NOT NULL DEFAULT FALSE )`); } catch (e) { console.warn('[GET /api/messages/:contactId] ensure table failed:', e.message); } try { const sql = `SELECT * FROM messages WHERE (sender_id = $1 AND receiver_id = $2) OR (sender_id = $2 AND receiver_id = $1) ORDER BY created_at ASC`; const messagesRes = await virtualWorldPool.query(sql, [userId, contactId]); res.json(messagesRes.rows); } catch (err) { console.error('[GET /api/messages/:contactId] error:', err); res.status(500).json({ error: 'Ошибка получения сообщений' }); } }); app.get('/api/messages-read/:contactId', authenticate, async (req, res) => { const userId = req.user.id; const contactId = parseInt(req.params.contactId, 10); try { // Проверяем есть ли НЕпрочитанные сообщения от этого контакта const sql = `SELECT EXISTS ( SELECT 1 FROM messages WHERE sender_id = $1 AND receiver_id = $2 AND is_read = false AND sender_id != receiver_id ) as has_unread`; const result = await virtualWorldPool.query(sql, [contactId, userId]); // Если есть непрочитанные - возвращаем "true", иначе "false" const hasUnread = result.rows[0].has_unread; res.json(hasUnread ? "true" : "false"); } catch (err) { console.error('[GET /api/messages-read/:contactId] error:', err); res.status(500).json({ error: 'Ошибка проверки состояния' }); } }); app.post('/api/messages-read-true-false', authenticate, async (req, res) => { try { const userId = req.user.id; const { contactId } = req.body; if (!contactId) { return res.status(400).json({ error: 'contactId required' }); } // Проверяем что contactId не равен ID текущего пользователя if (parseInt(contactId) === userId) { return res.json({ success: true, skipped: 'Нельзя отмечать свои собственные сообщения' }); } // Отмечаем сообщения как прочитанные ТОЛЬКО если отправитель ≠ получатель await virtualWorldPool.query( `UPDATE messages SET is_read = true WHERE sender_id = $1 AND receiver_id = $2 AND sender_id != receiver_id`, // Добавляем проверку [contactId, userId] ); res.json({ success: true }); } catch (error) { console.error('Error updating read status:', error); res.status(500).json({ error: 'Internal server error' }); } }); app.post('/api/messages/send', authenticate, async (req, res) => { const senderId = req.user.id; const { receiverId, message } = req.body || {}; const recvId = parseInt(receiverId, 10); console.log('[POST /api/messages/send] sender:', senderId, 'receiver:', recvId); try { const receiverCheck = await db.query('SELECT id FROM users WHERE id = $1', [recvId]); if (receiverCheck.rows.length === 0) { return res.status(404).json({ error: 'Пользователь не найден' }); } try { await virtualWorldPool.query(` CREATE TABLE IF NOT EXISTS messages ( id SERIAL PRIMARY KEY, sender_id INT NOT NULL, receiver_id INT NOT NULL, message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_read BOOLEAN NOT NULL DEFAULT FALSE )`); } catch (e) { console.warn('[POST /api/messages/send] ensure table failed:', e.message); } const result = await virtualWorldPool.query( `INSERT INTO messages (sender_id, receiver_id, message, created_at) VALUES ($1, $2, $3, NOW()) RETURNING id, created_at, is_read`, [senderId, recvId, message] ); const newMessage = result.rows[0]; const receiverSocketId = onlineUsers.get(recvId); if (receiverSocketId) { io.to(receiverSocketId).emit('newMessage', { id: newMessage.id, text: message, senderId, timestamp: newMessage.created_at, isRead: newMessage.is_read }); } res.status(201).json(newMessage); } catch (err) { console.error('[POST /api/messages/send] error:', err); res.status(500).json({ error: 'Ошибка отправки сообщения' }); } }); app.get('/api/messages', authenticate, async (req, res) => { const userId = req.user.id; try { await virtualWorldPool.query(` CREATE TABLE IF NOT EXISTS messages ( id SERIAL PRIMARY KEY, sender_id INT NOT NULL, receiver_id INT NOT NULL, message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_read BOOLEAN NOT NULL DEFAULT FALSE )`); } catch (e) { console.warn('[GET /api/messages] ensure table failed:', e.message); } try { const messagesRes = await virtualWorldPool.query( `SELECT * FROM messages WHERE sender_id = $1 OR receiver_id = $1 ORDER BY created_at DESC`, [userId] ); if (messagesRes.rows.length === 0) { return res.json([]); } const userIds = new Set(); messagesRes.rows.forEach(msg => { userIds.add(msg.sender_id); userIds.add(msg.receiver_id); }); const usersRes = await virtualWorldPool.query( `SELECT id, first_name, last_name, avatar_url FROM users WHERE id = ANY($1)`, [Array.from(userIds)] ); const userMap = {}; usersRes.rows.forEach(user => { userMap[user.id] = { name: `${user.first_name} ${user.last_name}`, avatar: user.avatar_url }; }); const messages = messagesRes.rows.map(msg => ({ id: msg.id, text: msg.message, senderId: msg.sender_id, receiverId: msg.receiver_id, sender: userMap[msg.sender_id] || { name: 'Неизвестный', avatar: null }, receiver: userMap[msg.receiver_id] || { name: 'Неизвестный', avatar: null }, timestamp: msg.created_at, isRead: msg.is_read })); res.json(messages); } catch (err) { console.error('[GET /api/messages] error:', err); res.status(500).json({ error: 'Ошибка получения сообщений' }); } }); app.patch('/api/messages/:id/read', authenticate, async (req, res) => { const messageId = req.params.id; const userId = req.user.id; try { await virtualWorldPool.query(` CREATE TABLE IF NOT EXISTS messages ( id SERIAL PRIMARY KEY, sender_id INT NOT NULL, receiver_id INT NOT NULL, message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_read BOOLEAN NOT NULL DEFAULT FALSE )`); } catch (e) { console.warn('[PATCH /api/messages/:id/read] ensure table failed:', e.message); } try { const checkRes = await virtualWorldPool.query(`SELECT id FROM messages WHERE id = $1 AND receiver_id = $2`, [messageId, userId]); if (checkRes.rows.length === 0) { return res.status(404).json({ error: 'Сообщение не найдено или доступ запрещен' }); } await virtualWorldPool.query(`UPDATE messages SET is_read = true WHERE id = $1`, [messageId]); res.status(204).end(); } catch (err) { console.error('[PATCH /api/messages/:id/read] error:', err); res.status(500).json({ error: 'Ошибка обновления сообщения' }); } }); app.get('/api/me', authenticate, async (req, res) => { const userId = req.user.id; const { rows } = await db.query(` SELECT email, first_name AS "firstName", last_name AS "lastName", gender, age, city, avatar_url AS "avatarURL", balance, hours_played AS "hoursPlayed", reputation, phone, sportiness, health_level AS "healthLevel", stress_level AS "stressLevel", satiety, thirst, diseases FROM users WHERE id = $1 `, [userId]); if (!rows.length) return res.status(404).json({ error: 'User not found' }); const user = rows[0]; // Автоматически исправляем неправильный avatarURL if (!user.avatarURL || user.avatarURL === 'try' || user.avatarURL === 'undefined' || user.avatarURL === 'null') { console.log(`Исправляем неправильный avatarURL для пользователя ${userId}: ${user.avatarURL} -> /models/character.glb`); try { await db.query( 'UPDATE users SET avatar_url = $1 WHERE id = $2', ['/models/character.glb', userId] ); user.avatarURL = '/models/character.glb'; } catch (e) { console.error('Ошибка обновления avatarURL:', e); } } res.json(user); }); app.get('/api/players/:socketId', authenticate, async (req, res) => { const socketId = req.params.socketId; let p = players[socketId]; if (!p) { for (const city of Object.values(playersByCity)) { if (city[socketId]) { p = city[socketId]; break; } } } if (!p) return res.status(404).json({ error: 'Player not found' }); const dbId = p.userId; if (!dbId) return res.status(404).json({ error: 'User profile missing' }); const { rows } = await db.query(` SELECT first_name AS "firstName", last_name AS "lastName", gender, age, city, avatar_url AS "avatarURL", balance, hours_played AS "hoursPlayed", reputation, phone, sportiness, health_level AS "healthLevel", stress_level AS "stressLevel", satiety, thirst, diseases FROM users WHERE id = $1 `, [dbId]); if (!rows.length) return res.status(404).json({ error: 'User not found in database' }); const user = rows[0]; // Автоматически исправляем неправильный avatarURL if (!user.avatarURL || user.avatarURL === 'try' || user.avatarURL === 'undefined' || user.avatarURL === 'null') { console.log(`Исправляем неправильный avatarURL для игрока ${dbId}: ${user.avatarURL} -> /models/character.glb`); try { await db.query( 'UPDATE users SET avatar_url = $1 WHERE id = $2', ['/models/character.glb', dbId] ); user.avatarURL = '/models/character.glb'; } catch (e) { console.error('Ошибка обновления avatarURL:', e); } } res.json(user); }); app.post('/api/register', async (req, res) => { try { console.log('register request:', req.body?.email); const { email, password, firstName, lastName, gender, age, city, avatarURL } = req.body || {}; if (!email || !password || !firstName || !lastName) { return res.status(400).json({ error: 'Не заполнены обязательные поля' }); } const { rowCount } = await db.query(`SELECT 1 FROM users WHERE email = $1`, [email]); if (rowCount) return res.status(400).json({ error: 'Почта уже занята' }); const hash = await bcrypt.hash(password, 10); const insertSQL = ` INSERT INTO users(email, password_hash, first_name, last_name, gender, age, city, avatar_url) VALUES($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id, email, created_at `; const result = await db.query(insertSQL, [ email, hash, firstName, lastName, gender ?? null, age ?? null, city ?? null, avatarURL ?? null ]); const user = result.rows[0]; // Не даём регистрации упасть, если экономика не завелась try { await Economy.createAccount(user.id, 'USD'); } catch (e) { console.error('Economy.createAccount failed:', e); } if (!process.env.JWT_SECRET) { console.error('JWT_SECRET не задан в окружении (.env)'); return res.status(500).json({ error: 'Ошибка конфигурации сервера' }); } const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '12h' }); res.json({ success: true, token }); } catch (e) { console.error('Ошибка регистрации:', e); res.status(500).json({ error: 'Внутренняя ошибка регистрации' }); } }); app.post('/api/login', async (req, res) => { const { email, password } = req.body; const { rows } = await db.query( `SELECT id, password_hash, first_name AS "firstName", last_name AS "lastName", gender, age, city, avatar_url AS "avatarURL" FROM users WHERE email = $1`, [email] ); if (!rows.length) { return res.status(401).json({ error: 'Неверный логин или пароль' }); } const user = rows[0]; const ok = await bcrypt.compare(password, user.password_hash); if (!ok) { return res.status(401).json({ error: 'Неверный логин или пароль' }); } const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '12h' }); res.json({ token, profile: { id: user.id, email, firstName: user.firstName, lastName: user.lastName, gender: user.gender, age: user.age, city: user.city, avatarURL: user.avatarURL } }); }); // Получить список доступных моделей app.get('/api/models', authenticate, async (req, res) => { try { const fs = require('fs'); const path = require('path'); const modelsDir = path.join(__dirname, 'public', 'models', 'copied'); if (!fs.existsSync(modelsDir)) { return res.json([]); } const files = fs.readdirSync(modelsDir); const modelFiles = files.filter(file => file.toLowerCase().endsWith('.glb') || file.toLowerCase().endsWith('.gltf') ); console.log('📁 Найдено моделей:', modelFiles.length); res.json(modelFiles); } catch (error) { console.error('Ошибка получения списка моделей:', error); res.status(500).json({ error: 'Ошибка получения списка моделей' }); } }); // Получить объекты города по cityId app.get('/api/cities/:cityId/objects', authenticate, async (req, res) => { const cityId = req.params.cityId; try { const { rows } = await db.query(` SELECT id, name, model_url, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, COALESCE(scale_x, 1) AS scale_x, COALESCE(scale_y, 1) AS scale_y, COALESCE(scale_z, 1) AS scale_z, organization_id, COALESCE(rent, 0) AS rent, COALESCE(tax, 0) AS tax, COALESCE(collidable, false) AS collidable, COALESCE(interior_id, 101) AS interior_id, COALESCE(textures, '-') AS textures FROM city_objects WHERE city_id = $1 `, [cityId]); res.json(rows); } catch (e) { console.error('Ошибка в /api/cities/:cityId/objects:', e); res.status(500).json({ error: 'Ошибка получения объектов города' }); } }); // Получить список доступных моделей из public/models/copied app.get('/api/models', authenticate, async (req, res) => { try { const dir = pathLib.join(__dirname, 'public', 'models', 'copied'); const files = await fs.promises.readdir(dir); const glbs = files.filter(f => f.toLowerCase().endsWith('.glb')); res.json(glbs); } catch (e) { res.status(500).json({ error: 'Ошибка чтения списка моделей' }); } }); // Обновить avatarURL пользователя app.put('/api/profile/avatar', authenticate, async (req, res) => { const { avatarURL } = req.body; const userId = req.user.id; try { // Проверяем, что avatarURL не пустой и валидный if (!avatarURL || avatarURL === 'try' || avatarURL === 'undefined' || avatarURL === 'null') { return res.status(400).json({ error: 'Неправильный avatarURL' }); } await db.query( 'UPDATE users SET avatar_url = $1 WHERE id = $2', [avatarURL, userId] ); res.json({ success: true, avatarURL }); } catch (e) { console.error('Ошибка обновления avatarURL:', e); res.status(500).json({ error: 'Ошибка обновления avatarURL' }); } }); // Регистрируем маршрут на старте приложения: app.get( '/api/city_objects/:objectId/interior', authenticate, async (req, res) => { const objectId = parseInt(req.params.objectId, 10); try { const { rows } = await db.query( 'SELECT interior_id FROM city_objects WHERE id = $1', [objectId] ); if (rows.length === 0) { return res.status(404).json({ error: 'Объект с таким id не найден' }); } res.json({ interiorId: rows[0].interior_id }); } catch (e) { console.error('Ошибка в /api/city_objects/:objectId/interior', e); res.status(500).json({ error: 'Не удалось получить interior_id' }); } } ); // Новый эндпоинт для входа в интерьер: app.post('/api/interiors/:interiorId/enter', authenticate, async (req, res) => { const interiorId = parseInt(req.params.interiorId, 10); try { const base = await db.query( 'SELECT spawn_x, spawn_y, spawn_z, spawn_rot, exit_x, exit_y, exit_z, exit_rot FROM interiors WHERE id = $1', [interiorId] ); const interior = base.rows[0]; if (!interior) return res.status(404).json({ error: 'Интерьер не найден' }); let exitInt = null; try { const extra = await db.query( 'SELECT exit_int_x, exit_int_y, exit_int_z FROM interiors WHERE id = $1', [interiorId] ); if (extra.rows[0] && (extra.rows[0].exit_int_x != null || extra.rows[0].exit_int_y != null || extra.rows[0].exit_int_z != null)) { exitInt = { x: extra.rows[0].exit_int_x, y: extra.rows[0].exit_int_y, z: extra.rows[0].exit_int_z }; } } catch (e2) { // Колонки exit_int_* могут отсутствовать — это не ошибка для клиента console.warn('exit_int_* columns are not available yet:', e2.message); } // Эффективная точка входа: если spawn_x/y/z не заданы (или равны 0), // пробуем использовать внутреннюю точку выхода (exit_int_*), // иначе в крайнем случае используем координаты внешнего выхода. const rawSpawn = { x: interior.spawn_x, y: interior.spawn_y, z: interior.spawn_z, rot: interior.spawn_rot }; const isZeroOrNull = v => v == null || Number(v) === 0; const spawnLooksEmpty = isZeroOrNull(rawSpawn.x) && isZeroOrNull(rawSpawn.y) && isZeroOrNull(rawSpawn.z); let effectiveSpawn = { ...rawSpawn }; if (spawnLooksEmpty) { if (exitInt && typeof exitInt.x === 'number' && typeof exitInt.z === 'number') { effectiveSpawn = { x: exitInt.x, y: exitInt.y ?? 0, z: exitInt.z, rot: rawSpawn.rot }; } else if (!isZeroOrNull(interior.exit_x) || !isZeroOrNull(interior.exit_y) || !isZeroOrNull(interior.exit_z)) { // Фолбэк на внешние координаты выхода, если они заданы effectiveSpawn = { x: interior.exit_x, y: interior.exit_y, z: interior.exit_z, rot: rawSpawn.rot }; } } // Логируем вычисленные координаты для отладки try { console.log('[INTERIOR ENTER]', { interiorId, spawn: effectiveSpawn, rawSpawn, exit: { x: interior.exit_x, y: interior.exit_y, z: interior.exit_z, rot: interior.exit_rot }, exitInt }); } catch (_) {} res.json({ spawn: effectiveSpawn, exit: { x: interior.exit_x, y: interior.exit_y, z: interior.exit_z, rot: interior.exit_rot }, exitInt }); } catch (e) { console.error(e); res.status(500).json({ error: 'Не удалось получить координаты интерьера' }); } }); // server.js, после маршрута /api/city_objects/:objectId/interior app.get('/api/interiors/:interiorId/definition', authenticate, async (req, res) => { const interiorId = parseInt(req.params.interiorId, 10); try { const interior = (await db.query( 'SELECT glb_filename, pos_x, pos_y, pos_z, spawn_x, spawn_y, spawn_z, spawn_rot FROM interiors WHERE id = $1', [interiorId] )).rows[0]; if (!interior) return res.status(404).json({ error: 'Интерьер не найден' }); const objects = (await db.query( `SELECT type, model_url, x, y, z, rot_x, rot_y, rot_z, scale FROM interior_objects WHERE interior_id = $1 ORDER BY id`, [interiorId] )).rows; res.json({ glb: `/models/interiors/${interior.glb_filename}`, position: { x: interior.pos_x, y: interior.pos_y, z: interior.pos_z }, spawn: { x: interior.spawn_x, y: interior.spawn_y, z: interior.spawn_z, rot: interior.spawn_rot }, objects }); } catch (e) { console.error(e); res.status(500).json({ error: 'Не удалось загрузить определение интерьера' }); } }); // Начало копи app.post('/api/listen', authenticate, async (req, res) => { try { console.log('[API /api/listen] Request body:', req.body); const { player_id: bodyPlayerId, json_filename } = req.body || {}; const authUser = req.user || {}; const effectivePlayerId = bodyPlayerId || authUser.email || authUser.id || authUser.userId; if (!effectivePlayerId || !json_filename) { return res.status(400).json({ success: false, error: 'player_id and json_filename are required' }); } // Выбираем пул: если есть отдельный пул, берём его, иначе общий db try { // Создаём таблицу при необходимости await virtualWorldPool.query(` CREATE TABLE IF NOT EXISTS json_listened ( id SERIAL PRIMARY KEY, player_id TEXT NOT NULL, json_filename TEXT NOT NULL, listened_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`); } catch (e) { console.warn('[API /api/listen] ensure table failed:', e.message); } const q = `INSERT INTO json_listened (player_id, json_filename, listened_at) VALUES ($1, $2, NOW())`; await virtualWorldPool.query(q, [String(effectivePlayerId), String(json_filename)]); console.log('[API /api/listen] Saved listened:', { player: effectivePlayerId, json: json_filename }); return res.status(200).json({ success: true }); } catch (err) { console.error('[API /api/listen] error:', err); return res.status(500).json({ success: false, error: 'Database operation failed', details: err.message }); } }); //Конец копи //Начало копи function generateTransactions() { const transactions = []; const suspiciousCount = Math.min(3 + Math.floor(level / 3), 5); const suspiciousIds = []; while (suspiciousIds.length < suspiciousCount) { const id = Math.floor(Math.random() * 15); if (!suspiciousIds.includes(id)) { suspiciousIds.push(id); } } // Генерируем 15 транзакций for (let i = 0; i < 15; i++) { const isSuspicious = suspiciousIds.includes(i); // ... остальной код генерации транзакции ... } return transactions; } // Завершение игры app.post('/api/cleanup-game/finish', authenticate, async (req, res) => { try { const { success, markedTransactions, personalArchive } = req.body; const userId = req.user.id; // Здесь должна быть логика обновления прогресса игрока // Например, увеличение репутации, разблокировка квестов и т.д. res.json({ success: true }); } catch (e) { res.status(500).json({ error: 'Ошибка сохранения результата игры' }); } }); // Вспомогательная функция для генерации транзакций function getRandomCity() { const cities = ["Москва", "Санкт-Петербург", "Новосибирск", "Екатеринбург", "Казань", "Самара", "Омск", "Челябинск", "Ростов-на-Дону", "Уфа"]; return cities[Math.floor(Math.random() * cities.length)]; } function getRandomIP() { return `${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`; } function getRandomDevice() { const devices = ["Chrome Win", "Safari iOS", "Android", "Firefox Mac", "Edge Win", "Opera Win"]; return devices[Math.floor(Math.random() * devices.length)]; } function getRandomPurpose() { const purposes = [ "Покупка продуктов", "Оплата услуг", "Перевод другу", "Оплата аренды", "Покупка техники", "Благотворительность", "Образовательные курсы" ]; return purposes[Math.floor(Math.random() * purposes.length)]; } function getRandomRecipient() { const recipients = [ "Пятёрочка №17", "ИП Сидоров И.И.", "OOO 'Комплекс-С'", "ИП Петрова А.А.", "Магнит №45", "Ашан Супермаркет", "ООО 'ТехноПрофи'" ]; return recipients[Math.floor(Math.random() * recipients.length)]; } function getSuspiciousIP() { // Генерируем IP из известных VPN диапазонов или Tor exit nodes const vpnRanges = [ "185.2.33.", "213.42.12.", "172.16.", "192.168.", "10.0." ]; const range = vpnRanges[Math.floor(Math.random() * vpnRanges.length)]; return range + Math.floor(Math.random() * 255); } function getSuspiciousDevice() { // Подозрительные устройства - одинаковые для разных транзакций const suspiciousDevices = [ "Tor Browser", "Android 4.4.2 (старая версия)", "iPhone 6 (iOS 10)", "Emulator Android" ]; return suspiciousDevices[Math.floor(Math.random() * suspiciousDevices.length)]; } // Обновленная функция генерации транзакций function generateTransactions() { const transactions = []; const suspiciousIds = [0, 5, 10]; // Фиксированные индексы подозрительных транзакций // Генерируем 15 транзакций for (let i = 0; i < 15; i++) { const isSuspicious = suspiciousIds.includes(i); const baseDate = new Date(2023, 6, 25); // 25 июля 2023 let date, amount, purpose, ip, city, device, recipient; let anomalyType = isSuspicious ? i % 3 : null; // 0, 1 или 2 для подозрительных if (isSuspicious) { date = new Date(baseDate.getTime() + Math.floor(i / 5) * 24 * 60 * 60 * 1000); amount = `₽${Math.floor(200000 + Math.random() * 800000).toLocaleString()}`; switch (anomalyType) { case 0: // Географический прыжок date.setHours(date.getHours() + 1); ip = getSuspiciousIP(); city = i % 2 === 0 ? "Москва" : "Самара"; device = getRandomDevice(); purpose = getRandomPurpose(); recipient = getRandomRecipient(); break; case 1: // Пустое назначение + VPN ip = getSuspiciousIP(); city = getRandomCity(); device = getSuspiciousDevice(); purpose = ''; recipient = getRandomRecipient(); break; case 2: // Повтор получателя ip = getRandomIP(); city = getRandomCity(); device = getRandomDevice(); purpose = getRandomPurpose(); recipient = "ООО 'Сомнительные Переводы'"; break; } } else { // Нормальные транзакции date = new Date(baseDate.getTime() + Math.floor(i / 5) * 24 * 60 * 60 * 1000); amount = `₽${Math.floor(1000 + Math.random() * 20000).toLocaleString()}`; purpose = getRandomPurpose(); ip = getRandomIP(); city = getRandomCity(); device = getRandomDevice(); recipient = getRandomRecipient(); } transactions.push({ id: i, date: date.toLocaleDateString(), time: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), amount, purpose: isSuspicious && Math.random() > 0.5 ? '' : purpose, ip: isSuspicious && Math.random() > 0.7 ? '' : ip, city, device: isSuspicious && Math.random() > 0.5 ? '' : device, recipient, _realIp: isSuspicious ? getSuspiciousIP() : ip, _realDevice: isSuspicious ? getSuspiciousDevice() : device, _isSuspicious: isSuspicious, _anomalyType: anomalyType }); } return transactions; } // Начало кода для квестов // Маршрут для получения информации о квестах игрока app.get('/api/quests/player-status', authenticate, async (req, res) => { try { const userId = req.user.id; console.log(`[QUESTS] Запрос статуса квестов для пользователя ${userId}`); // Получаем уровень игрока (по умолчанию 1) const playerLevel = 1; // Пока используем фиксированный уровень // Получаем все активные квесты с их статусами для игрока const questsData = await getPlayerQuestsData(userId, playerLevel); console.log(`[QUESTS] Возвращаем данные для пользователя ${userId}: ${questsData.length} квестов`); res.json({ success: true, playerLevel: playerLevel, quests: questsData }); } catch (error) { console.error('Ошибка получения статуса квестов игрока:', error); res.status(500).json({ success: false, error: 'Ошибка получения данных о квестах' }); } }); // Вспомогательная функция для получения уровня игрока async function getPlayerLevel(userId) { try { // Пока используем уровень по умолчанию 1 // В будущем можно добавить логику расчета уровня на основе опыта return 1; } catch (error) { console.error('Ошибка получения уровня игрока:', error); return 1; } } // Функция для получения мок-данных квестов function getMockQuestsData() { return [ { id: 1, title: "Добро пожаловать в игру!", description: "Пройдите обучение и изучите основы игры", kind: "tutorial", status: "available", hasAccess: true, currentStep: { id: 1, stepIndex: 1, title: "Начало приключения", description: "Поговорите с гидом в центре города", playerStatus: "not_started" }, steps: [ { id: 1, stepIndex: 1, title: "Начало приключения", description: "Поговорите с гидом в центре города", actionType: "talk_to_npc", actionPayload: { npc_id: 1 }, dialogueScene: "tutorial_start", isOptional: false, playerStatus: "not_started", startedAt: null, completedAt: null }, { id: 2, stepIndex: 2, title: "Изучение интерфейса", description: "Откройте инвентарь и изучите интерфейс", actionType: "open_inventory", actionPayload: {}, dialogueScene: null, isOptional: false, playerStatus: "not_started", startedAt: null, completedAt: null } ], metadata: {}, startedAt: null, completedAt: null }, { id: 2, title: "Первое задание", description: "Выполните простое задание для получения опыта", kind: "main", status: "locked", hasAccess: false, currentStep: null, steps: [ { id: 3, stepIndex: 1, title: "Найти предмет", description: "Найдите потерянный предмет в городе", actionType: "find_item", actionPayload: { item_id: 1 }, dialogueScene: null, isOptional: false, playerStatus: "not_started", startedAt: null, completedAt: null } ], metadata: {}, startedAt: null, completedAt: null } ]; } // Основная функция для получения данных о квестах async function getPlayerQuestsData(userId, playerLevel) { try { // Проверяем подключение к базе данных if (!new_quest_Base) { console.error('[QUESTS] База данных квестов не инициализирована'); return getMockQuestsData(); } // Пробуем получить данные из базы данных try { const availableQuests = await new_quest_Base.query(` SELECT q.id, q.title, q.description, q.kind, q.is_active, q.metadata, pq.status as player_status, pq.current_step_id, pq.started_at, pq.completed_at FROM quests q LEFT JOIN player_quests pq ON q.id = pq.quest_id AND pq.player_id = $1 WHERE q.is_active = true ORDER BY q.id `, [userId]); const questsData = []; for (const quest of availableQuests.rows) { // Проверяем условия доступа к квесту const hasAccess = await checkQuestAccess(quest.id, playerLevel, userId); // Получаем информацию о текущем шаге const currentStepInfo = await getCurrentStepInfo(quest.id, quest.current_step_id, userId); // Получаем все шаги квеста const questSteps = await new_quest_Base.query(` SELECT qs.id, qs.step_index, qs.title, qs.description, qs.action_type, qs.action_payload, qs.dialogue_scene, qs.is_optional, ps.status as player_step_status, ps.started_at as step_started, ps.completed_at as step_completed FROM quest_steps qs LEFT JOIN player_steps ps ON qs.id = ps.quest_step_id AND ps.player_id = $1 WHERE qs.quest_id = $2 ORDER BY qs.step_index `, [userId, quest.id]); // Определяем статус квеста для игрока let questStatus = 'available'; // доступен if (quest.player_status === 'completed') { questStatus = 'completed'; } else if (quest.player_status === 'in_progress') { questStatus = 'in_progress'; } else if (!hasAccess) { questStatus = 'locked'; } questsData.push({ id: quest.id, title: quest.title, description: quest.description, kind: quest.kind, status: questStatus, hasAccess: hasAccess, currentStep: currentStepInfo, steps: questSteps.rows.map(step => ({ id: step.id, stepIndex: step.step_index, title: step.title, description: step.description, actionType: step.action_type, actionPayload: step.action_payload, dialogueScene: step.dialogue_scene, isOptional: step.is_optional, playerStatus: step.player_step_status || 'not_started', startedAt: step.step_started, completedAt: step.step_completed })), metadata: quest.metadata, startedAt: quest.started_at, completedAt: quest.completed_at }); } return questsData; } catch (dbError) { console.error('Ошибка получения данных квестов из базы данных:', dbError.message); console.log('[QUESTS] Используем мок-данные для квестов'); return getMockQuestsData(); } } catch (error) { console.error('Ошибка получения данных квестов:', error); // Возвращаем мок-данные вместо пустого массива return getMockQuestsData(); } } // Функция проверки доступа к квесту async function checkQuestAccess(questId, playerLevel, userId) { try { console.log('Проверка доступа к квесту:', { questId, playerLevel, userId }); // Проверяем подключение к базе данных if (!new_quest_Base) { console.error('База данных квестов не инициализирована'); return false; } // Проверяем группы условий let prerequisiteGroups; try { prerequisiteGroups = await new_quest_Base.query(` SELECT qpg.id, qpg.group_index FROM quest_prereq_groups qpg WHERE qpg.quest_id = $1 ORDER BY qpg.group_index `, [questId]); } catch (dbError) { console.log('Ошибка доступа к таблице условий квестов, разрешаем доступ:', dbError.message); // Если база данных недоступна, разрешаем доступ return true; } // Если нет групп условий - квест доступен if (prerequisiteGroups.rows.length === 0) { console.log('Квест доступен (нет условий доступа)'); return true; } // Проверяем каждую группу условий for (const group of prerequisiteGroups.rows) { const groupConditions = await new_quest_Base.query(` SELECT qpc.condition_type, qpc.condition_payload FROM quest_prereq_conditions qpc WHERE qpc.group_id = $1 `, [group.id]); let allConditionsMet = true; for (const condition of groupConditions.rows) { const conditionMet = await checkCondition( condition.condition_type, condition.condition_payload, playerLevel, userId ); if (!conditionMet) { allConditionsMet = false; break; } } // Если хотя бы одна группа условий выполнена - квест доступен if (allConditionsMet) { return true; } } return false; } catch (error) { console.error('Ошибка проверки доступа к квесту:', error); return false; } } // Функция проверки конкретного условия async function checkCondition(conditionType, conditionPayload, playerLevel, userId) { try { const payload = typeof conditionPayload === 'string' ? JSON.parse(conditionPayload) : conditionPayload; switch (conditionType) { case 'level_ge': return playerLevel >= payload.level; case 'quest_completed': // Проверяем завершенность другого квеста const questCheck = await new_quest_Base.query(` SELECT 1 FROM player_quests WHERE player_id = $1 AND quest_id = $2 AND status = 'completed' `, [userId, payload.quest_id]); return questCheck.rows.length > 0; case 'step_completed': // Проверяем завершенность шага const stepCheck = await new_quest_Base.query(` SELECT 1 FROM player_steps WHERE player_id = $1 AND quest_step_id = $2 AND status = 'completed' `, [userId, payload.step_id]); return stepCheck.rows.length > 0; default: console.warn(`Неизвестный тип условия: ${conditionType}`); return false; } } catch (error) { console.error('Ошибка проверки условия:', error); return false; } } // Функция получения информации о текущем шаге async function getCurrentStepInfo(questId, currentStepId, userId) { try { if (!currentStepId) { // Если текущего шага нет, возвращаем первый шаг квеста const firstStep = await new_quest_Base.query(` SELECT id, step_index, title, description FROM quest_steps WHERE quest_id = $1 ORDER BY step_index ASC LIMIT 1 `, [questId]); return firstStep.rows.length > 0 ? { id: firstStep.rows[0].id, stepIndex: firstStep.rows[0].step_index, title: firstStep.rows[0].title, description: firstStep.rows[0].description } : null; } // Получаем информацию о текущем шаге const currentStep = await new_quest_Base.query(` SELECT qs.id, qs.step_index, qs.title, qs.description, ps.status as player_status FROM quest_steps qs LEFT JOIN player_steps ps ON qs.id = ps.quest_step_id AND ps.player_id = $1 WHERE qs.id = $2 `, [userId, currentStepId]); return currentStep.rows.length > 0 ? { id: currentStep.rows[0].id, stepIndex: currentStep.rows[0].step_index, title: currentStep.rows[0].title, description: currentStep.rows[0].description, playerStatus: currentStep.rows[0].player_status } : null; } catch (error) { console.error('Ошибка получения информации о шаге:', error); return null; } } // Маршрут для старта квеста app.post('/api/quests/:questId/start', authenticate, async (req, res) => { try { const userId = req.user.id; const questId = parseInt(req.params.questId); console.log('Запрос начала квеста:', { userId, questId }); const playerLevel = await getPlayerLevel(userId); // Проверяем существование квеста в базе данных let questExists; try { questExists = await new_quest_Base.query(` SELECT id FROM quests WHERE id = $1 `, [questId]); } catch (dbError) { console.log('Ошибка доступа к базе данных квестов, используем мок-данные:', dbError.message); // Если база данных недоступна, разрешаем квест с ID 1 if (questId === 1) { questExists = { rows: [{ id: 1 }] }; } else { return res.status(404).json({ success: false, error: 'Квест не найден' }); } } if (questExists.rows.length === 0) { console.log('Квест не найден в базе данных:', questId); return res.status(404).json({ success: false, error: 'Квест не найден' }); } // Проверяем доступность квеста console.log('Проверяем доступ к квесту:', { questId, playerLevel, userId }); const hasAccess = await checkQuestAccess(questId, playerLevel, userId); console.log('Результат проверки доступа:', hasAccess); if (!hasAccess) { return res.status(403).json({ success: false, error: 'Квест недоступен' }); } // Проверяем, не начат ли уже квест let existingQuest; try { existingQuest = await new_quest_Base.query(` SELECT id FROM player_quests WHERE player_id = $1 AND quest_id = $2 `, [userId, questId]); } catch (dbError) { console.log('Ошибка доступа к таблице квестов игроков, пропускаем проверку:', dbError.message); existingQuest = { rows: [] }; // Предполагаем, что квест не начат } if (existingQuest.rows.length > 0) { return res.status(400).json({ success: false, error: 'Квест уже начат' }); } // Получаем первый шаг квеста let firstStep; try { firstStep = await new_quest_Base.query(` SELECT id FROM quest_steps WHERE quest_id = $1 ORDER BY step_index ASC LIMIT 1 `, [questId]); } catch (dbError) { console.log('Ошибка доступа к таблице шагов квестов, создаем мок-шаг:', dbError.message); // Создаем мок-шаг firstStep = { rows: [{ id: 1 }] }; } if (firstStep.rows.length === 0) { return res.status(400).json({ success: false, error: 'Квест не имеет шагов' }); } // Начинаем квест try { await new_quest_Base.query(` INSERT INTO player_quests (player_id, quest_id, current_step_id, status, started_at, last_updated_at) VALUES ($1, $2, $3, 'in_progress', NOW(), NOW()) `, [userId, questId, firstStep.rows[0].id]); // Записываем первый шаг await new_quest_Base.query(` INSERT INTO player_steps (player_id, quest_step_id, status, started_at) VALUES ($1, $2, 'in_progress', NOW()) `, [userId, firstStep.rows[0].id]); } catch (dbError) { console.log('Ошибка записи квеста в базу данных, но продолжаем:', dbError.message); // Продолжаем выполнение, даже если не удалось записать в БД } res.json({ success: true, message: 'Квест начат', currentStepId: firstStep.rows[0].id }); } catch (error) { console.error('Ошибка старта квеста:', error); res.status(500).json({ success: false, error: 'Ошибка начала квеста' }); } }); // Маршрут для отметки прослушанного диалога app.post('/api/quests/mark-dialog-listened', authenticate, async (req, res) => { try { const userId = req.user.id; const { npc_id, dialogue_key } = req.body; console.log(`[QUESTS] Отметка прослушанного диалога: user=${userId}, npc=${npc_id}, dialogue=${dialogue_key}`); if (!npc_id || !dialogue_key) { return res.status(400).json({ success: false, error: 'Не указаны npc_id или dialogue_key' }); } // 1. Записываем взаимодействие в player_npc_interactions const interactionResult = await new_quest_Base.query(` INSERT INTO player_npc_interactions (player_id, npc_id, action_type, action_payload, created_at) VALUES ($1, $2, 'listen_npc', $3, NOW()) RETURNING id `, [userId, npc_id, JSON.stringify({ dialogue_key: dialogue_key })]); console.log(`[QUESTS] Взаимодействие записано с ID: ${interactionResult.rows[0].id}`); // 2. Проверяем, относится ли это взаимодействие к активному шагу квеста const activeQuestStep = await new_quest_Base.query(` SELECT pq.id as player_quest_id, pq.current_step_id, qs.id as step_id, qs.action_payload FROM player_quests pq JOIN quest_steps qs ON pq.current_step_id = qs.id WHERE pq.player_id = $1 AND pq.status = 'in_progress' AND qs.action_type = 'talk_npc' `, [userId]); if (activeQuestStep.rows.length > 0) { const step = activeQuestStep.rows[0]; const actionPayload = typeof step.action_payload === 'string' ? JSON.parse(step.action_payload) : step.action_payload; // Проверяем, соответствует ли NPC текущему шагу квеста if (actionPayload.npc_id == npc_id) { console.log(`[QUESTS] Диалог соответствует активному шагу квеста: step_id=${step.step_id}`); // 3. Проверяем требования для завершения шага const stepRequirements = await new_quest_Base.query(` SELECT requirement_type, requirement_payload FROM step_requirements WHERE quest_step_id = $1 ORDER BY ord `, [step.step_id]); let stepCompleted = true; for (const requirement of stepRequirements.rows) { const payload = typeof requirement.requirement_payload === 'string' ? JSON.parse(requirement.requirement_payload) : requirement.requirement_payload; if (requirement.requirement_type === 'listen_npc') { // Проверяем, прослушал ли игрок нужное количество диалогов с указанными NPC const listenCount = await new_quest_Base.query(` SELECT COUNT(*) as count FROM player_npc_interactions WHERE player_id = $1 AND npc_id = ANY($2::int[]) AND action_type = 'listen_npc' `, [userId, payload.npc_ids]); if (listenCount.rows[0].count < payload.count) { stepCompleted = false; break; } } } if (stepCompleted) { // 4. Отмечаем шаг как завершенный await new_quest_Base.query(` UPDATE player_steps SET status = 'completed', completed_at = NOW() WHERE player_id = $1 AND quest_step_id = $2 `, [userId, step.step_id]); // 5. Находим следующий шаг const nextStep = await new_quest_Base.query(` SELECT qs.id, qs.step_index FROM quest_steps qs WHERE qs.quest_id = ( SELECT quest_id FROM quest_steps WHERE id = $1 ) AND qs.step_index > ( SELECT step_index FROM quest_steps WHERE id = $1 ) ORDER BY qs.step_index ASC LIMIT 1 `, [step.step_id]); if (nextStep.rows.length > 0) { // 6. Обновляем текущий шаг в квесте await new_quest_Base.query(` UPDATE player_quests SET current_step_id = $1, last_updated_at = NOW() WHERE player_id = $2 AND id = $3 `, [nextStep.rows[0].id, userId, step.player_quest_id]); // 7. Начинаем следующий шаг await new_quest_Base.query(` INSERT INTO player_steps (player_id, quest_step_id, status, started_at) VALUES ($1, $2, 'in_progress', NOW()) ON CONFLICT (player_id, quest_step_id) DO UPDATE SET status = 'in_progress', started_at = NOW() `, [userId, nextStep.rows[0].id]); console.log(`[QUESTS] Шаг завершен, переход к шагу ${nextStep.rows[0].step_index}`); } else { // 8. Если это последний шаг - завершаем квест await new_quest_Base.query(` UPDATE player_quests SET status = 'completed', completed_at = NOW(), last_updated_at = NOW() WHERE player_id = $1 AND id = $2 `, [userId, step.player_quest_id]); console.log(`[QUESTS] Квест завершен!`); } } } } res.json({ success: true, message: 'Диалог отмечен как прослушанный' }); } catch (error) { console.error('Ошибка при отметке прослушанного диалога:', error); res.status(500).json({ success: false, error: 'Ошибка сервера при обработке диалога' }); } }); //Конец копи //Начало копи app.get('/api/quests/progress', authenticate, async (req, res) => { console.log("Загрузка на сервере. ID пользователя:", req.user.id); try { // ВМЕСТО получения email, используем напрямую ID пользователя const userId = req.user.id.toString(); // Преобразуем в строку для consistency // Получаем список всех квестов с их JSON файлами const questsQuery = await virtualWorldPool.query(` SELECT q.id, q.title, qj.json_filename FROM quests q JOIN quest_jsons qj ON q.id = qj.quest_id ORDER BY q.id `); // Получаем JSON файлы, которые прослушал игрок по его ID const listenedQuery = await virtualWorldPool.query(` SELECT json_filename FROM json_listened WHERE player_id = $1 `, [userId]); // Используем ID вместо email console.log("Результат запроса listenedQuery для ID", userId, ":", listenedQuery.rows); const listenedFiles = new Set(listenedQuery.rows.map(row => row.json_filename)); console.log("Прослушанные файлы:", Array.from(listenedFiles)); // Остальной код без изменений... const questsMap = new Map(); questsQuery.rows.forEach(row => { if (!questsMap.has(row.id)) { questsMap.set(row.id, { id: row.id, title: row.title, total: 0, completed: 0, files: [] }); } const quest = questsMap.get(row.id); quest.total++; quest.files.push(row.json_filename); if (listenedFiles.has(row.json_filename)) { quest.completed++; } }); const result = Array.from(questsMap.values()).map(quest => ({ id: quest.id, title: quest.title, progress: quest.total > 0 ? Math.round((quest.completed / quest.total) * 100) : 0, completed: quest.completed, total: quest.total })); console.log("Результат для клиента:", result); res.json(result); } catch (err) { console.error('Ошибка получения прогресса квестов:', err); res.status(500).json({ error: 'Ошибка получения прогресса квестов' }); } }); app.get('/api/cleanup-game/data', authenticate, async (req, res) => { try { const level = parseInt(req.query.level) || 1; // Генерируем транзакции с учетом уровня const transactions = generateTransactions(level); res.json({ success: true, transactions: transactions, level: level }); } catch (e) { console.error('Ошибка генерации данных игры:', e); res.status(500).json({ success: false, error: 'Ошибка генерации данных игры' }); } }); //Конец копи // Список интерьеров с координатами для отображения на карте app.get('/api/interiors', authenticate, async (req, res) => { try { const { rows } = await db.query( 'SELECT id, pos_x, pos_y, pos_z FROM interiors ORDER BY id' ); res.json(rows); } catch (e) { console.error('Ошибка получения списка интерьеров', e); res.status(500).json({ error: 'Не удалось получить список интерьеров' }); } }); // Получить объекты интерьера app.get('/api/interiors/:id/objects', authenticate, async (req, res) => { const id = parseInt(req.params.id, 10); try { const { rows } = await db.query( `SELECT id, model_url, x, y, z, rot_x, rot_y, rot_z, scale FROM interior_objects WHERE interior_id = $1 ORDER BY id`, [id] ); res.json(rows); } catch (e) { console.error('Ошибка получения объектов интерьера', e); res.status(500).json({ error: 'Не удалось получить объекты интерьера' }); } }); // Сохранить объекты интерьера в БД app.post('/api/interiors/:id/save', authenticate, async (req, res) => { const id = parseInt(req.params.id, 10); const { objects = [], removedIds = [] } = req.body; if (!Array.isArray(objects) || !Array.isArray(removedIds)) { return res.status(400).json({ error: 'Invalid objects' }); } try { if (removedIds.length) { await db.query( 'DELETE FROM interior_objects WHERE id = ANY($1::int[]) AND interior_id = $2', [removedIds, id] ); } for (const obj of objects) { if (obj.id) { await db.query( `UPDATE interior_objects SET model_url=$1, x=$2, y=$3, z=$4, rot_x=$5, rot_y=$6, rot_z=$7, scale=$8 WHERE id=$9 AND interior_id=$10`, [ obj.model_url, obj.x, obj.y, obj.z, obj.rot_x, obj.rot_y, obj.rot_z, obj.scale, obj.id, id ] ); } else { await db.query( `INSERT INTO interior_objects (interior_id, model_url, x, y, z, rot_x, rot_y, rot_z, scale) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, [ id, obj.model_url, obj.x, obj.y, obj.z, obj.rot_x, obj.rot_y, obj.rot_z, obj.scale ] ); } } res.json({ ok: true }); } catch (e) { console.error('Ошибка сохранения интерьера', e); res.status(500).json({ error: 'Не удалось сохранить интерьер' }); } }); // Коллизии карты: загрузка/сохранение файла public/colliders.json app.get('/api/colliders', authenticate, async (req, res) => { const cityId = Number(req.query.cityId) || 0; try { const fileName = cityId ? `colliders_city_${cityId}.json` : 'colliders.json'; const filePath = pathLib.join(__dirname, 'public', fileName); try { const raw = await fs.promises.readFile(filePath, 'utf8'); const json = JSON.parse(raw); return res.json(json); } catch (e) { // Если нет файла — создаём пустой const empty = { colliders: [] }; await fs.promises.mkdir(pathLib.join(__dirname, 'public'), { recursive: true }); await fs.promises.writeFile(filePath, JSON.stringify(empty, null, 2), 'utf8'); return res.json(empty); } } catch (e) { console.error('Ошибка чтения colliders.json:', e); res.status(500).json({ error: 'Ошибка чтения коллизий' }); } }); app.post('/api/colliders', authenticate, async (req, res) => { try { const { colliders, cityId } = req.body || {}; if (!Array.isArray(colliders)) { return res.status(400).json({ error: 'Invalid colliders' }); } const fileName = cityId ? `colliders_city_${Number(cityId)}.json` : 'colliders.json'; const filePath = pathLib.join(__dirname, 'public', fileName); await fs.promises.mkdir(pathLib.join(__dirname, 'public'), { recursive: true }); await fs.promises.writeFile(filePath, JSON.stringify({ colliders }, null, 2), 'utf8'); res.json({ ok: true }); } catch (e) { console.error('Ошибка записи colliders.json:', e); res.status(500).json({ error: 'Ошибка сохранения коллизий' }); } }); // Получить организацию по objectId app.get('/api/organizations/by-object/:objectId', authenticate, async (req, res) => { const objectId = parseInt(req.params.objectId, 10); try { const { rows } = await db.query(` SELECT o.id, o.name, os.menu, os.work_hours FROM city_objects AS co JOIN organizations AS o ON co.organization_id = o.id JOIN organization_settings AS os ON os.organization_id = o.id WHERE co.id = $1 `, [objectId]); if (rows.length === 0) { return res.status(404).json({ error: 'Организация не найдена для этого объекта' }); } res.json(rows[0]); } catch (e) { console.error('Ошибка в /api/organizations/by-object/:objectId:', e); res.status(500).json({ error: 'Ошибка получения меню организации' }); } }); // Сохранить текущую карту в текстовый файл app.post('/api/save-map', authenticate, async (req, res) => { const { cityId = 'unknown', objects, removedIds = [] } = req.body; if (!Array.isArray(objects) || !Array.isArray(removedIds)) { return res.status(400).json({ error: 'Invalid objects' }); } try { const dir = pathLib.join(__dirname, 'saves'); await fs.promises.mkdir(dir, { recursive: true }); const file = `city_${cityId}_${Date.now()}.txt`; const filePath = pathLib.join(dir, file); await fs.promises.writeFile( filePath, JSON.stringify({ objects, removedIds }, null, 2), 'utf8' ); res.json({ ok: true, file }); } catch (e) { console.error('Ошибка сохранения карты', e); res.status(500).json({ error: 'Ошибка сохранения карты' }); } }); // Получить список городов с названием и страной app.get('/api/cities', authenticate, async (req, res) => { try { const { rows } = await db.query(` SELECT cities.id, cities.name, countries.name AS country_name FROM cities JOIN countries ON cities.country_id = countries.id ORDER BY countries.name, cities.name `); res.json(rows); } catch (e) { res.status(500).json({ error: 'Ошибка получения списка городов' }); } }); // Сохранить/обновить объект города в БД app.post('/api/save-object', authenticate, async (req, res) => { try { const { id, city_id, name, model_url, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, scale_x, scale_y, scale_z, organization_id = 2, rent = 0, tax = 0, collidable = false, interior_id = 101, textures = '-' } = req.body; console.log('🔍 Получены данные объекта:', { id, name, collidable, city_id }); if (!city_id || !model_url) { return res.status(400).json({ error: 'city_id и model_url обязательны' }); } let result; if (id && id !== null && id !== undefined) { // Обновление существующего объекта console.log('🔄 Обновляем существующий объект с ID:', id); console.log('🔍 Значение collidable для UPDATE:', collidable); const { rows } = await db.query(` UPDATE city_objects SET name = $1, model_url = $2, pos_x = $3, pos_y = $4, pos_z = $5, rot_x = $6, rot_y = $7, rot_z = $8, scale_x = $9, scale_y = $10, scale_z = $11, organization_id = $12, rent = $13, tax = $14, collidable = $15, interior_id = $16, textures = $17 WHERE id = $18 RETURNING id `, [ name || '', model_url, pos_x || 0, pos_y || 0, pos_z || 0, rot_x || 0, rot_y || 0, rot_z || 0, scale_x || 1, scale_y || 1, scale_z || 1, organization_id, rent, tax, collidable, interior_id, textures, id ]); result = rows[0]; console.log('✅ Объект обновлен:', result); } else { // Создание нового объекта console.log('🆕 Создаем новый объект'); console.log('🔍 Значение collidable для INSERT:', collidable); const { rows } = await db.query(` INSERT INTO city_objects ( city_id, name, model_url, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, scale_x, scale_y, scale_z, organization_id, rent, tax, collidable, interior_id, textures ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id `, [ city_id, name || '', model_url, pos_x || 0, pos_y || 0, pos_z || 0, rot_x || 0, rot_y || 0, rot_z || 0, scale_x || 1, scale_y || 1, scale_z || 1, organization_id, rent, tax, collidable, interior_id, textures ]); result = rows[0]; console.log('✅ Новый объект создан:', result); } res.json({ id: result.id, success: true }); } catch (error) { console.error('Ошибка сохранения объекта:', error); res.status(500).json({ error: 'Ошибка сохранения объекта' }); } }); // Удалить объект города из БД app.delete('/api/delete-object/:id', authenticate, async (req, res) => { try { const objectId = parseInt(req.params.id, 10); if (!objectId) { return res.status(400).json({ error: 'ID объекта обязателен' }); } const { rowCount } = await db.query( 'DELETE FROM city_objects WHERE id = $1', [objectId] ); if (rowCount === 0) { return res.status(404).json({ error: 'Объект не найден' }); } res.json({ success: true, message: 'Объект удален' }); } catch (error) { console.error('Ошибка удаления объекта:', error); res.status(500).json({ error: 'Ошибка удаления объекта' }); } }); // Тестовый эндпоинт для проверки таблицы city_objects app.get('/api/test-city-objects', authenticate, async (req, res) => { try { const { rows } = await db.query(` SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_name = 'city_objects' ORDER BY ordinal_position `); res.json({ tableExists: rows.length > 0, columns: rows }); } catch (error) { console.error('Ошибка проверки таблицы:', error); res.status(500).json({ error: 'Ошибка проверки таблицы', details: error.message }); } }); // API endpoint для получения коллайдеров города из базы данных app.get('/api/colliders/city/:cityId', authenticate, async (req, res) => { const cityId = parseInt(req.params.cityId, 10); try { const { rows } = await db.query(` SELECT id, type, position_x as "position.x", position_y as "position.y", position_z as "position.z", rotation_x as "rotation.x", rotation_y as "rotation.y", rotation_z as "rotation.z", scale_x as "scale.x", scale_y as "scale.y", scale_z as "scale.z", color_r as "color.r", color_g as "color.g", color_b as "color.b", opacity, created_at, updated_at FROM colliders WHERE city_id = $1 ORDER BY created_at ASC `, [cityId]); // Преобразуем данные в формат, ожидаемый фронтендом const colliders = rows.map(row => ({ id: row.id, type: row.type, position: { x: parseFloat(row["position.x"]), y: parseFloat(row["position.y"]), z: parseFloat(row["position.z"]) }, rotation: { x: parseFloat(row["rotation.x"]), y: parseFloat(row["rotation.y"]), z: parseFloat(row["rotation.z"]) }, scale: { x: parseFloat(row["scale.x"]), y: parseFloat(row["scale.y"]), z: parseFloat(row["scale.z"]) }, color: { r: parseFloat(row["color.r"]), g: parseFloat(row["color.g"]), b: parseFloat(row["color.b"]) }, opacity: parseFloat(row.opacity), created_at: row.created_at, updated_at: row.updated_at })); res.json({ colliders }); } catch (error) { console.error('Ошибка получения коллайдеров из БД:', error); res.status(500).json({ error: 'Ошибка получения коллайдеров' }); } }); // API endpoint для сохранения коллайдеров города в базу данных app.post('/api/colliders/city/:cityId', authenticate, async (req, res) => { const cityId = parseInt(req.params.cityId, 10); const { colliders } = req.body; if (!Array.isArray(colliders)) { return res.status(400).json({ error: 'Invalid colliders data' }); } try { // Начинаем транзакцию await db.query('BEGIN'); // Разделяем коллайдеры на новые и существующие const newColliders = colliders.filter(c => !c.id); const existingColliders = colliders.filter(c => c.id); console.log(`💾 Сохраняем: ${existingColliders.length} существующих, ${newColliders.length} новых коллайдеров`); // Обновляем существующие коллайдеры for (const collider of existingColliders) { await db.query(` UPDATE colliders SET type = $2, position_x = $3, position_y = $4, position_z = $5, rotation_x = $6, rotation_y = $7, rotation_z = $8, scale_x = $9, scale_y = $10, scale_z = $11, color_r = $12, color_g = $13, color_b = $14, opacity = $15, updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND city_id = $16 `, [ collider.id, collider.type, collider.position?.x || 0, collider.position?.y || 0, collider.position?.z || 0, collider.rotation?.x || 0, collider.rotation?.y || 0, collider.rotation?.z || 0, collider.scale?.x || 1, collider.scale?.y || 1, collider.scale?.z || 1, collider.color?.r || 1, collider.color?.g || 0, collider.color?.b || 0, collider.opacity || 0.3, cityId ]); } // Вставляем новые коллайдеры for (const collider of newColliders) { await db.query(` INSERT INTO colliders ( city_id, type, position_x, position_y, position_z, rotation_x, rotation_y, rotation_z, scale_x, scale_y, scale_z, color_r, color_g, color_b, opacity ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 ) `, [ cityId, collider.type, collider.position?.x || 0, collider.position?.y || 0, collider.position?.z || 0, collider.rotation?.x || 0, collider.rotation?.y || 0, collider.rotation?.z || 0, collider.scale?.x || 1, collider.scale?.y || 1, collider.scale?.z || 1, collider.color?.r || 1, collider.color?.g || 0, collider.color?.b || 0, collider.opacity || 0.3 ]); } // Подтверждаем транзакцию await db.query('COMMIT'); console.log(`✅ Коллайдеры для города ${cityId} сохранены в БД (${existingColliders.length} обновлено, ${newColliders.length} новых)`); res.json({ success: true, message: 'Коллайдеры сохранены успешно', updated: existingColliders.length, created: newColliders.length }); } catch (error) { // Откатываем транзакцию в случае ошибки await db.query('ROLLBACK'); console.error('Ошибка сохранения коллайдеров в БД:', error); res.status(500).json({ error: 'Ошибка сохранения коллайдеров' }); } }); // API endpoint для обновления отдельного коллайдера app.put('/api/colliders/:colliderId', authenticate, async (req, res) => { const colliderId = parseInt(req.params.colliderId, 10); const collider = req.body; try { const { rowCount } = await db.query(` UPDATE colliders SET type = $2, position_x = $3, position_y = $4, position_z = $5, rotation_x = $6, rotation_y = $7, rotation_z = $8, scale_x = $9, scale_y = $10, scale_z = $11, color_r = $12, color_g = $13, color_b = $14, opacity = $15, updated_at = CURRENT_TIMESTAMP WHERE id = $1 `, [ colliderId, collider.type, collider.position?.x || 0, collider.position?.y || 0, collider.position?.z || 0, collider.rotation?.x || 0, collider.rotation?.y || 0, collider.rotation?.z || 0, collider.scale?.x || 1, collider.scale?.y || 1, collider.scale?.z || 1, collider.color?.r || 1, collider.color?.g || 0, collider.color?.b || 0, collider.opacity || 0.3 ]); if (rowCount === 0) { return res.status(404).json({ error: 'Коллайдер не найден' }); } console.log(`✅ Коллайдер ${colliderId} обновлен в БД`); res.json({ success: true, message: 'Коллайдер обновлен успешно' }); } catch (error) { console.error('Ошибка обновления коллайдера в БД:', error); res.status(500).json({ error: 'Ошибка обновления коллайдера' }); } }); // API endpoint для удаления отдельного коллайдера app.delete('/api/colliders/:colliderId', authenticate, async (req, res) => { const colliderId = parseInt(req.params.colliderId, 10); try { const { rowCount } = await db.query('DELETE FROM colliders WHERE id = $1', [colliderId]); if (rowCount === 0) { return res.status(404).json({ error: 'Коллайдер не найден' }); } console.log(`✅ Коллайдер ${colliderId} удален из БД`); res.json({ success: true, message: 'Коллайдер удален успешно' }); } catch (error) { console.error('Ошибка удаления коллайдера из БД:', error); res.status(500).json({ error: 'Ошибка удаления коллайдера' }); } }); app.use((req, res) => { res.sendFile(pathLib.join(__dirname, 'build', 'index.html')); }); const PORT = process.env.PORT || 4000; 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]; app[method] = function(path, ...args) { //if (typeof path === 'string') { // console.log(`Регистрируется ${method.toUpperCase()} маршрут:`, path); //} else if (typeof path === 'function') { // console.log(`Регистрируется middleware (без пути) через ${method}`); //} return orig.call(this, path, ...args); }; }); // После ensureMessagesTable(); async function ensureInteriorsSpawnColumns() { try { await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_x NUMERIC DEFAULT 0'); await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_y NUMERIC DEFAULT 0'); await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_z NUMERIC DEFAULT 0'); await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_rot NUMERIC DEFAULT 0'); await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_x NUMERIC DEFAULT 0'); await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_y NUMERIC DEFAULT 0'); await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_z NUMERIC DEFAULT 0'); await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_rot NUMERIC DEFAULT 0'); } catch (e) { console.error('Ошибка добавления spawn/exit-колонок в interiors', e); } } ensureInteriorsSpawnColumns(); // Добавляем колонки масштаба для городских объектов, если ещё не созданы async function ensureCityObjectsScaleColumns() { try { await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_x NUMERIC DEFAULT 1'); await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_y NUMERIC DEFAULT 1'); await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_z NUMERIC DEFAULT 1'); } catch (e) { console.error('Ошибка добавления scale колонок в city_objects', e); } } ensureCityObjectsScaleColumns();