2836 lines
105 KiB
JavaScript
2836 lines
105 KiB
JavaScript
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(); |