Interiors with TG Fix MID
This commit is contained in:
BIN
public/models/npc/galina.glb
Normal file
BIN
public/models/npc/galina.glb
Normal file
Binary file not shown.
375
server.js
375
server.js
@@ -138,6 +138,7 @@ io.on('connection', socket => {
|
||||
socketId: socket.id,
|
||||
userId: socket.userId,
|
||||
x,
|
||||
y: 0,
|
||||
z,
|
||||
cityId,
|
||||
avatarURL: null,
|
||||
@@ -148,6 +149,7 @@ io.on('connection', socket => {
|
||||
players[socket.id] = playersByCity[cityId][socket.id];
|
||||
socket.cityId = cityId;
|
||||
socket.x = x;
|
||||
socket.y = 0;
|
||||
socket.z = z;
|
||||
// Отправляем только игроков этого города
|
||||
socket.emit('currentPlayers', playersByCity[cityId]);
|
||||
@@ -155,11 +157,12 @@ io.on('connection', socket => {
|
||||
|
||||
// --- Новый игрок ---
|
||||
socket.on('newPlayer', data => {
|
||||
const cityId = data.cityId || socket.cityId || 1;
|
||||
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,
|
||||
@@ -170,9 +173,83 @@ io.on('connection', socket => {
|
||||
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,
|
||||
@@ -183,38 +260,10 @@ io.on('connection', socket => {
|
||||
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;
|
||||
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,
|
||||
z: movementData.z
|
||||
});
|
||||
}
|
||||
}
|
||||
// Voice chat nearby только в этом городе
|
||||
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.emit('currentPlayers', playersByCity[cityId]);
|
||||
});
|
||||
|
||||
socket.on('sendMessage', async ({ receiverId, message }, callback) => {
|
||||
@@ -365,46 +414,68 @@ app.get('/api/users', authenticate, async (req, res) => {
|
||||
app.get('/api/messages/:contactId', authenticate, async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const contactId = parseInt(req.params.contactId, 10);
|
||||
const pool = (typeof virtualWorldPool !== 'undefined' && virtualWorldPool) ? virtualWorldPool : db;
|
||||
try {
|
||||
// Ensure table exists
|
||||
await pool.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 messagesRes = await virtualWorldPool.query(
|
||||
`SELECT * FROM messages
|
||||
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`,
|
||||
[userId, contactId]
|
||||
);
|
||||
|
||||
ORDER BY created_at ASC`;
|
||||
const messagesRes = await pool.query(sql, [userId, contactId]);
|
||||
res.json(messagesRes.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error('[GET /api/messages/:contactId] error:', err);
|
||||
res.status(500).json({ error: 'Ошибка получения сообщений' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/messages/send', authenticate, async (req, res) => {
|
||||
const senderId = req.user.id;
|
||||
const { receiverId, message } = req.body;
|
||||
const { receiverId, message } = req.body || {};
|
||||
const recvId = parseInt(receiverId, 10);
|
||||
console.log("Запрос пошел");
|
||||
const pool = (typeof virtualWorldPool !== 'undefined' && virtualWorldPool) ? virtualWorldPool : db;
|
||||
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 pool.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);
|
||||
}
|
||||
|
||||
// Сохранение сообщения в virtual_world
|
||||
const result = await virtualWorldPool.query(
|
||||
const result = await pool.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[recvId];
|
||||
if (receiverSocketId) {
|
||||
io.to(receiverSocketId).emit('newMessage', {
|
||||
@@ -415,55 +486,49 @@ app.post('/api/messages/send', authenticate, async (req, res) => {
|
||||
isRead: newMessage.is_read
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json(newMessage);
|
||||
} catch (err) {
|
||||
console.error(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;
|
||||
|
||||
const pool = (typeof virtualWorldPool !== 'undefined' && virtualWorldPool) ? virtualWorldPool : db;
|
||||
try {
|
||||
// Получение сообщений из virtual_world
|
||||
const messagesRes = await virtualWorldPool.query(
|
||||
await pool.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 pool.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([]);
|
||||
}
|
||||
|
||||
// Сбор ID пользователей
|
||||
const userIds = new Set();
|
||||
messagesRes.rows.forEach(msg => {
|
||||
userIds.add(msg.sender_id);
|
||||
userIds.add(msg.receiver_id);
|
||||
});
|
||||
|
||||
// Получение данных пользователей из основной БД
|
||||
messagesRes.rows.forEach(msg => { userIds.add(msg.sender_id); userIds.add(msg.receiver_id); });
|
||||
const usersRes = await db.query(
|
||||
`SELECT id, first_name, last_name, avatar_url
|
||||
FROM users
|
||||
WHERE id = ANY($1)`,
|
||||
`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
|
||||
};
|
||||
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,
|
||||
@@ -474,10 +539,9 @@ app.get('/api/messages', authenticate, async (req, res) => {
|
||||
timestamp: msg.created_at,
|
||||
isRead: msg.is_read
|
||||
}));
|
||||
|
||||
res.json(messages);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error('[GET /api/messages] error:', err);
|
||||
res.status(500).json({ error: 'Ошибка получения сообщений' });
|
||||
}
|
||||
});
|
||||
@@ -485,27 +549,29 @@ app.get('/api/messages', authenticate, async (req, res) => {
|
||||
app.patch('/api/messages/:id/read', authenticate, async (req, res) => {
|
||||
const messageId = req.params.id;
|
||||
const userId = req.user.id;
|
||||
|
||||
const pool = (typeof virtualWorldPool !== 'undefined' && virtualWorldPool) ? virtualWorldPool : db;
|
||||
try {
|
||||
// Проверка прав доступа
|
||||
const checkRes = await virtualWorldPool.query(
|
||||
`SELECT id FROM messages WHERE id = $1 AND receiver_id = $2`,
|
||||
[messageId, userId]
|
||||
);
|
||||
|
||||
await pool.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 pool.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]
|
||||
);
|
||||
|
||||
await pool.query(`UPDATE messages SET is_read = true WHERE id = $1`, [messageId]);
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error('[PATCH /api/messages/:id/read] error:', err);
|
||||
res.status(500).json({ error: 'Ошибка обновления сообщения' });
|
||||
}
|
||||
});
|
||||
@@ -787,24 +853,73 @@ app.get(
|
||||
app.post('/api/interiors/:interiorId/enter', authenticate, async (req, res) => {
|
||||
const interiorId = parseInt(req.params.interiorId, 10);
|
||||
try {
|
||||
const interior = (await db.query(
|
||||
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]
|
||||
)).rows[0];
|
||||
);
|
||||
const interior = base.rows[0];
|
||||
if (!interior) return res.status(404).json({ error: 'Интерьер не найден' });
|
||||
res.json({
|
||||
spawn: {
|
||||
|
||||
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);
|
||||
@@ -844,30 +959,37 @@ app.get('/api/interiors/:interiorId/definition', authenticate, async (req, res)
|
||||
|
||||
// Начало копи
|
||||
app.post('/api/listen', authenticate, async (req, res) => {
|
||||
console.log('Request data:', req.body);
|
||||
const { player_id, json_filename } = req.body;
|
||||
|
||||
if (!player_id || !json_filename) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'player_id and json_filename are required'
|
||||
});
|
||||
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
|
||||
const pool = (typeof virtualWorldPool !== 'undefined' && virtualWorldPool) ? virtualWorldPool : db;
|
||||
try {
|
||||
await virtualWorldPool.query(`
|
||||
INSERT INTO json_listened (player_id, json_filename, listened_at)
|
||||
VALUES ($1, $2, NOW())
|
||||
`, [player_id, json_filename]);
|
||||
// Создаём таблицу при необходимости
|
||||
await pool.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);
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
const q = `INSERT INTO json_listened (player_id, json_filename, listened_at) VALUES ($1, $2, NOW())`;
|
||||
await pool.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('Full DB error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Database operation failed',
|
||||
details: process.env.NODE_ENV === 'development' ? err.message : null
|
||||
});
|
||||
console.error('[API /api/listen] error:', err);
|
||||
return res.status(500).json({ success: false, error: 'Database operation failed', details: err.message });
|
||||
}
|
||||
});
|
||||
//Конец копи
|
||||
@@ -1047,15 +1169,19 @@ app.get('/api/quests/progress', authenticate, async (req, res) => {
|
||||
console.log("Загрузка на сервере. ID пользователя:", req.user.id);
|
||||
|
||||
try {
|
||||
// Получаем email пользователя из основной БД
|
||||
// Получаем email пользователя из основной БД (или из токена)
|
||||
const fallbackEmail = req.user?.email || req.user?.username || null;
|
||||
const userRes = await db.query('SELECT email FROM users WHERE id = $1', [req.user.id]);
|
||||
if (userRes.rows.length === 0) {
|
||||
if (!fallbackEmail) {
|
||||
return res.status(404).json({ error: 'Пользователь не найден' });
|
||||
}
|
||||
const userEmail = userRes.rows[0].email;
|
||||
}
|
||||
const userEmail = userRes.rows[0]?.email || fallbackEmail;
|
||||
|
||||
// Получаем список всех квестов с их JSON файлами
|
||||
const questsQuery = await virtualWorldPool.query(`
|
||||
const pool = (typeof virtualWorldPool !== 'undefined' && virtualWorldPool) ? virtualWorldPool : db;
|
||||
const questsQuery = await pool.query(`
|
||||
SELECT q.id, q.title, qj.json_filename
|
||||
FROM quests q
|
||||
JOIN quest_jsons qj ON q.id = qj.quest_id
|
||||
@@ -1063,7 +1189,20 @@ app.get('/api/quests/progress', authenticate, async (req, res) => {
|
||||
`);
|
||||
|
||||
// Получаем JSON файлы, которые прослушал игрок
|
||||
const listenedQuery = await virtualWorldPool.query(`
|
||||
// Гарантируем наличие таблицы json_listened
|
||||
try {
|
||||
await pool.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/quests/progress] ensure table json_listened failed:', e.message);
|
||||
}
|
||||
|
||||
const listenedQuery = await pool.query(`
|
||||
SELECT json_filename FROM json_listened
|
||||
WHERE player_id = $1
|
||||
`, [userEmail]);
|
||||
|
||||
487
src/Game.js
487
src/Game.js
@@ -24,11 +24,15 @@ function Game({ avatarUrl, gender }) {
|
||||
|
||||
// 3) реф для группы «интерьера»
|
||||
const interiorGroupRef = useRef(null);
|
||||
const interiorCollidersRef = useRef([]);
|
||||
const interiorExitPosRef = useRef(null);
|
||||
const fpHiddenNodesRef = useRef([]);
|
||||
const cleanupTimerRef = useRef(null);
|
||||
// Глобальный менеджер прогресса загрузки (используем в GLTFLoader)
|
||||
const loadingManagerRef = useRef(null);
|
||||
// Кликабельные объекты внутри интерьера
|
||||
const interiorInteractablesRef = useRef([]);
|
||||
const npcMeshesRef = useRef([]);
|
||||
|
||||
// камеры
|
||||
const orthoCamRef = useRef(null);
|
||||
@@ -39,6 +43,7 @@ function Game({ avatarUrl, gender }) {
|
||||
const fpPitchRef = useRef(0);
|
||||
const isTouchDevice = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
|
||||
const isInInteriorRef = useRef(false);
|
||||
const altHeldRef = useRef(false);
|
||||
const LOAD_RADIUS = 120;
|
||||
|
||||
const [activeApp, setActiveApp] = useState(null);
|
||||
@@ -111,6 +116,8 @@ function Game({ avatarUrl, gender }) {
|
||||
|
||||
const [seregaComments, setSeregaComments] = useState([]);
|
||||
const [currentExit, setCurrentExit] = useState(null);
|
||||
const currentExitRef = useRef(null);
|
||||
useEffect(() => { currentExitRef.current = currentExit; }, [currentExit]);
|
||||
|
||||
useEffect(() => {
|
||||
const decay = setInterval(() => {
|
||||
@@ -496,7 +503,7 @@ function Game({ avatarUrl, gender }) {
|
||||
// базовая геометрия для объектов типа "chair"
|
||||
const baseChairMesh = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 1, 1),
|
||||
new THREE.MeshStandardMaterial({ color: 0x888888 })
|
||||
new THREE.MeshBasicMaterial({ visible: false })
|
||||
);
|
||||
|
||||
async function loadGLTF(url) {
|
||||
@@ -522,10 +529,8 @@ function Game({ avatarUrl, gender }) {
|
||||
switchToFirstPersonCamera();
|
||||
|
||||
// Включаем управление мышью для интерьера
|
||||
document.body.style.cursor = 'none'; // Скрываем курсор
|
||||
if (rendererRef.current) {
|
||||
rendererRef.current.domElement.requestPointerLock();
|
||||
}
|
||||
// Курсор оставляем активным (без pointer lock)
|
||||
document.body.style.cursor = 'default';
|
||||
|
||||
// Устанавливаем состояние "в интерьере"
|
||||
console.log('Устанавливаем setIsInInterior(true)');
|
||||
@@ -533,10 +538,26 @@ function Game({ avatarUrl, gender }) {
|
||||
setSelectedHouse(null);
|
||||
|
||||
console.log('isInInterior установлен в true');
|
||||
// Сброс кликово-путевого движения и визуальных маркеров
|
||||
if (typeof currentPath !== 'undefined') currentPath = [];
|
||||
if (typeof pathIndex !== 'undefined') pathIndex = 0;
|
||||
if (typeof destination !== 'undefined') destination = null;
|
||||
if (typeof blockedTime !== 'undefined') blockedTime = 0;
|
||||
if (typeof destinationMarker !== 'undefined' && destinationMarker) destinationMarker.visible = false;
|
||||
// Сброс нажатых направлений
|
||||
if (moveInputRef.current) {
|
||||
Object.keys(moveInputRef.current).forEach(k => { moveInputRef.current[k] = false; });
|
||||
}
|
||||
|
||||
// Телепортируем игрока в интерьер (если нужно)
|
||||
console.log('Вызываем teleportPlayerToInterior для интерьера:', interiorId);
|
||||
await teleportPlayerToInterior(interiorId);
|
||||
// Отправляем мгновенное обновление позиции перед уведомлением об интерьере
|
||||
if (socketRef.current && playerRef.current) {
|
||||
socketRef.current.emit('playerMovement', { x: playerRef.current.position.x, y: playerRef.current.position.y, z: playerRef.current.position.z });
|
||||
}
|
||||
// Сообщаем серверу о смене интерьера, чтобы видимость игроков фильтровалась по interiorId
|
||||
socketRef.current?.emit('interiorChange', { interiorId });
|
||||
console.log('teleportPlayerToInterior завершена');
|
||||
}
|
||||
const teleportPlayerToInterior = async (interiorId) => {
|
||||
@@ -560,21 +581,54 @@ function Game({ avatarUrl, gender }) {
|
||||
alert(`Не удалось получить координаты интерьера: ${errText}`);
|
||||
return;
|
||||
}
|
||||
const { spawn, exit } = await res.json();
|
||||
const { spawn, exit, exitInt } = await res.json();
|
||||
if (!spawn) {
|
||||
alert('Для этого интерьера не заданы координаты входа');
|
||||
return;
|
||||
}
|
||||
// Нормализуем типы в числа (pg для NUMERIC отдает строки)
|
||||
const nSpawn = {
|
||||
x: Number(spawn.x),
|
||||
y: Number(spawn.y),
|
||||
z: Number(spawn.z),
|
||||
rot: Number(spawn.rot) || 0
|
||||
};
|
||||
const nExit = exit && typeof exit === 'object' ? {
|
||||
x: Number(exit.x),
|
||||
y: Number(exit.y),
|
||||
z: Number(exit.z),
|
||||
rot: Number(exit.rot) || 0
|
||||
} : null;
|
||||
const nExitInt = exitInt && typeof exitInt === 'object' ? {
|
||||
x: Number(exitInt.x),
|
||||
y: Number(exitInt.y),
|
||||
z: Number(exitInt.z)
|
||||
} : null;
|
||||
// Телепортируем игрока в интерьер
|
||||
if (playerRef.current) {
|
||||
playerRef.current.position.set(spawn.x, spawn.y, spawn.z);
|
||||
playerRef.current.rotation.set(0, spawn.rot || 0, 0);
|
||||
// Можно добавить сброс скорости, анимации и т.д. при необходимости
|
||||
console.log('[ENTER INTERIOR] spawn from server:', nSpawn);
|
||||
playerRef.current.position.set(nSpawn.x, nSpawn.y, nSpawn.z);
|
||||
playerRef.current.rotation.set(0, nSpawn.rot || 0, 0);
|
||||
// Полный сброс движения/целей при входе
|
||||
if (typeof currentPath !== 'undefined') currentPath = [];
|
||||
if (typeof pathIndex !== 'undefined') pathIndex = 0;
|
||||
if (typeof destination !== 'undefined') destination = null;
|
||||
if (typeof blockedTime !== 'undefined') blockedTime = 0;
|
||||
if (typeof destinationMarker !== 'undefined' && destinationMarker) destinationMarker.visible = false;
|
||||
if (moveInputRef.current) {
|
||||
Object.keys(moveInputRef.current).forEach(k => { moveInputRef.current[k] = false; });
|
||||
}
|
||||
setCurrentExit(exit || null);
|
||||
// Добавляем маркер выхода
|
||||
if (exit) {
|
||||
addExitMarker(exit);
|
||||
}
|
||||
console.log('[ENTER INTERIOR] exit from server:', nExit);
|
||||
setCurrentExit(nExit || null);
|
||||
// Визуализируем маркер выхода внутри интерьера, чтобы по клику можно было выйти
|
||||
if (nExit && typeof nExit.x === 'number' && typeof nExit.z === 'number') {
|
||||
try { addExitMarker(nExit); } catch (e) { console.warn('[ENTER INTERIOR] addExitMarker failed', e); }
|
||||
}
|
||||
// Запоминаем позицию внутреннего триггера выхода, если пришла
|
||||
if (nExitInt && typeof nExitInt.x === 'number') {
|
||||
console.log('[ENTER INTERIOR] exitInt (internal exit trigger):', nExitInt);
|
||||
interiorExitPosRef.current = new THREE.Vector3(nExitInt.x, nExitInt.y || 0, nExitInt.z);
|
||||
}
|
||||
console.log('teleportPlayerToInterior завершена успешно');
|
||||
} catch (e) {
|
||||
@@ -619,9 +673,54 @@ function Game({ avatarUrl, gender }) {
|
||||
intGroup.name = 'interiorGroup';
|
||||
intGroup.add(gltf.scene);
|
||||
|
||||
// Декуплируем и гарантируем непрозрачность материалов интерьера
|
||||
gltf.scene.traverse((child) => {
|
||||
if (child.isMesh && child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material = child.material.map(mat => {
|
||||
if (!mat) return mat;
|
||||
const m = mat.clone();
|
||||
m.transparent = false;
|
||||
m.opacity = 1;
|
||||
m.depthWrite = true;
|
||||
m.needsUpdate = true;
|
||||
return m;
|
||||
});
|
||||
} else {
|
||||
child.material = child.material.clone();
|
||||
child.material.transparent = false;
|
||||
child.material.opacity = 1;
|
||||
child.material.depthWrite = true;
|
||||
child.material.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Построение коллайдеров интерьера (простые коробки по мешам)
|
||||
const colliders = [];
|
||||
gltf.scene.traverse((child) => {
|
||||
if (child.isMesh && child.geometry) {
|
||||
colliders.push(child);
|
||||
}
|
||||
});
|
||||
interiorCollidersRef.current = colliders;
|
||||
|
||||
// Добавляем объекты интерьера
|
||||
interiorInteractablesRef.current = []; // сбрасываем реестр интерактива
|
||||
|
||||
// Хелпер для определения ID NPC по пути к модели
|
||||
const getNpcIdFromModel = (url) => {
|
||||
if (!url || typeof url !== 'string') return null;
|
||||
const lower = url.toLowerCase();
|
||||
if (lower.includes('/models/npc/galina.glb')) return 'Adventurer';
|
||||
if (lower.includes('/models/npc/oxranik.glb')) return 'Oxranik';
|
||||
if (lower.includes('/models/npc/guard.glb')) return 'guard';
|
||||
if (lower.includes('/models/npc/beachcharacter.glb')) return 'BeachCharacter';
|
||||
if (lower.includes('/models/npc/bartender.glb')) return 'bartender';
|
||||
if (lower.includes('/models/npc/computer.glb')) return 'Computer';
|
||||
return null;
|
||||
};
|
||||
|
||||
for (const o of objects) {
|
||||
if (o.model_url) {
|
||||
try {
|
||||
@@ -630,6 +729,34 @@ function Game({ avatarUrl, gender }) {
|
||||
objGltf.scene.rotation.set(o.rot_x, o.rot_y, o.rot_z);
|
||||
objGltf.scene.scale.set(o.scale, o.scale, o.scale);
|
||||
intGroup.add(objGltf.scene);
|
||||
|
||||
// Если это NPC внутри интерьера — добавим кликабельную хит‑зону
|
||||
const isNpc = (o.type === 'npc') || (typeof o.model_url === 'string' && o.model_url.includes('/models/npc/'));
|
||||
if (isNpc) {
|
||||
const npcId = o.id || getNpcIdFromModel(o.model_url);
|
||||
console.log('[INTERIOR NPC] detected npc, id:', npcId, 'at', { x: o.x, y: o.y, z: o.z });
|
||||
const hit = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(1.2),
|
||||
new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.0001, depthWrite: false })
|
||||
);
|
||||
hit.position.set(o.x, (o.y ?? 0) + 1.0, o.z);
|
||||
hit.userData.interactable = true;
|
||||
hit.userData.payload = { type: 'npc', id: npcId };
|
||||
hit.visible = true;
|
||||
intGroup.add(hit);
|
||||
interiorInteractablesRef.current.push(hit);
|
||||
|
||||
// Также помечаем сам корень модели как кликабельный NPC
|
||||
try {
|
||||
objGltf.scene.userData = objGltf.scene.userData || {};
|
||||
objGltf.scene.userData.interactable = true;
|
||||
objGltf.scene.userData.payload = { type: 'npc', id: npcId };
|
||||
interiorInteractablesRef.current.push(objGltf.scene);
|
||||
// и помечаем как isNpc/npcId для fallback
|
||||
objGltf.scene.userData.isNpc = true;
|
||||
objGltf.scene.userData.npcId = npcId;
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Не удалось загрузить объект интерьера', o.model_url, e);
|
||||
}
|
||||
@@ -638,21 +765,45 @@ function Game({ avatarUrl, gender }) {
|
||||
mesh.position.set(o.x, o.y, o.z);
|
||||
mesh.rotation.set(o.rot_x, o.rot_y, o.rot_z);
|
||||
mesh.scale.set(o.scale, o.scale, o.scale);
|
||||
if (mesh.material) {
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material = mesh.material.map(mat => {
|
||||
if (!mat) return mat;
|
||||
const m = mat.clone();
|
||||
m.transparent = false;
|
||||
m.opacity = 1;
|
||||
m.depthWrite = true;
|
||||
m.needsUpdate = true;
|
||||
return m;
|
||||
});
|
||||
} else {
|
||||
mesh.material = mesh.material.clone();
|
||||
mesh.material.transparent = false;
|
||||
mesh.material.opacity = 1;
|
||||
mesh.material.depthWrite = true;
|
||||
mesh.material.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
intGroup.add(mesh);
|
||||
}
|
||||
|
||||
// Если сервер прислал «маркер»/NPC — пометим кликабельным
|
||||
// Если сервер пометил объект как «интерактивный/маркер» — кликабельная зона
|
||||
if (o.interactable || o.marker) {
|
||||
const hit = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.6),
|
||||
new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.15, depthWrite: false })
|
||||
new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.0001, depthWrite: false })
|
||||
);
|
||||
hit.position.set(o.x, o.y + 1.0, o.z);
|
||||
hit.userData.interactable = true;
|
||||
hit.userData.payload = { type: o.type || 'marker', id: o.id || null, label: o.label || 'Интерактив' };
|
||||
hit.visible = true; // невидим визуально (opacity≈0), но кликабелен
|
||||
intGroup.add(hit);
|
||||
interiorInteractablesRef.current.push(hit);
|
||||
}
|
||||
// Сохраним позицию внутреннего выхода, если есть
|
||||
if (typeof o.exit_int_x === 'number' && typeof o.exit_int_y === 'number' && typeof o.exit_int_z === 'number') {
|
||||
interiorExitPosRef.current = new THREE.Vector3(o.exit_int_x, o.exit_int_y, o.exit_int_z);
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем освещение для интерьера
|
||||
@@ -931,10 +1082,33 @@ function Game({ avatarUrl, gender }) {
|
||||
const exitInterior = () => {
|
||||
console.log('exitInterior вызвана');
|
||||
|
||||
// Возвращаем игрока на исходную позицию (если не телепортировали)
|
||||
if (playerRef.current && savedPositionRef.current) {
|
||||
// Телепортируем на координаты выхода из интерьера, если заданы; иначе возвращаем на сохранённую позицию
|
||||
if (playerRef.current) {
|
||||
const cx = currentExitRef.current;
|
||||
console.log('[EXIT] currentExit before teleport:', cx);
|
||||
if (cx && typeof cx.x === 'number') {
|
||||
playerRef.current.position.set(
|
||||
cx.x,
|
||||
typeof cx.y === 'number' ? cx.y : playerRef.current.position.y,
|
||||
cx.z
|
||||
);
|
||||
playerRef.current.rotation.set(0, cx.rot || 0, 0);
|
||||
console.log('[EXIT] Teleported to exit coords');
|
||||
// Гарантируем выход из интерьера на сервере
|
||||
socketRef.current?.emit('interiorChange', { interiorId: null });
|
||||
// Включаем мир (закрытие могло скрыть город)
|
||||
try { toggleWorldVisibility(true); } catch (_) {}
|
||||
} else if (savedPositionRef.current) {
|
||||
console.log('[EXIT] No exit coords, using savedPositionRef');
|
||||
playerRef.current.position.copy(savedPositionRef.current);
|
||||
}
|
||||
// Сразу шлём позицию наружу
|
||||
socketRef.current?.emit('playerMovement', {
|
||||
x: playerRef.current.position.x,
|
||||
y: playerRef.current.position.y,
|
||||
z: playerRef.current.position.z
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем маркер выхода, если был
|
||||
if (window.exitMarkerMesh && sceneRef.current) {
|
||||
@@ -955,6 +1129,39 @@ function Game({ avatarUrl, gender }) {
|
||||
if (typeof updateCityObjectVisibility === 'function') {
|
||||
updateCityObjectVisibility();
|
||||
}
|
||||
// Повторно закрепляем телепорт на выход уже после очистки интерьера (на случай перезаписи позы)
|
||||
if (playerRef.current) {
|
||||
const cx2 = currentExitRef.current;
|
||||
console.log('[EXIT AFTER CLEANUP] currentExit:', cx2);
|
||||
if (cx2 && typeof cx2.x === 'number') {
|
||||
playerRef.current.position.set(
|
||||
cx2.x,
|
||||
typeof cx2.y === 'number' ? cx2.y : playerRef.current.position.y,
|
||||
cx2.z
|
||||
);
|
||||
playerRef.current.rotation.set(0, cx2.rot || 0, 0);
|
||||
console.log('[EXIT AFTER CLEANUP] Position applied');
|
||||
}
|
||||
if (typeof lastPlayerPosition !== 'undefined') {
|
||||
try { lastPlayerPosition = playerRef.current.position.clone(); } catch (_) {}
|
||||
}
|
||||
socketRef.current?.emit('playerMovement', {
|
||||
x: playerRef.current.position.x,
|
||||
y: playerRef.current.position.y,
|
||||
z: playerRef.current.position.z
|
||||
});
|
||||
}
|
||||
// Полный сброс путевого движения и ввода
|
||||
if (typeof currentPath !== 'undefined') currentPath = [];
|
||||
if (typeof pathIndex !== 'undefined') pathIndex = 0;
|
||||
if (typeof destination !== 'undefined') destination = null;
|
||||
if (typeof blockedTime !== 'undefined') blockedTime = 0;
|
||||
if (typeof destinationMarker !== 'undefined' && destinationMarker) destinationMarker.visible = false;
|
||||
if (moveInputRef.current) {
|
||||
Object.keys(moveInputRef.current).forEach(k => { moveInputRef.current[k] = false; });
|
||||
}
|
||||
// Сообщаем серверу, что покинули интерьер
|
||||
socketRef.current?.emit('interiorChange', { interiorId: null });
|
||||
|
||||
// Возвращаем курсор и отключаем pointer lock
|
||||
document.body.style.cursor = 'default';
|
||||
@@ -962,6 +1169,7 @@ function Game({ avatarUrl, gender }) {
|
||||
|
||||
setIsInInterior(false);
|
||||
setCurrentExit(null);
|
||||
interiorExitPosRef.current = null;
|
||||
};
|
||||
|
||||
|
||||
@@ -1361,8 +1569,20 @@ function switchToFirstPersonCamera() {
|
||||
console.log('Камера переключена на fpCamRef');
|
||||
}
|
||||
if (playerRef.current) {
|
||||
// Скрываем полностью собственную модель в режиме FPV
|
||||
playerRef.current.visible = false;
|
||||
console.log('Игрок скрыт');
|
||||
// На всякий случай также скрываем голову/шею (если модель будет вновь показана без выхода из режима)
|
||||
const hidden = [];
|
||||
playerRef.current.traverse((child) => {
|
||||
if (!child.isMesh) return;
|
||||
const name = (child.name || '').toLowerCase();
|
||||
if (name.includes('head') || name.includes('neck') || name.includes('helmet') || name.includes('hair')) {
|
||||
child.visible = false;
|
||||
hidden.push(child);
|
||||
}
|
||||
});
|
||||
fpHiddenNodesRef.current = hidden;
|
||||
console.log('Скрыты узлы для FPV:', hidden.map(n => n.name));
|
||||
}
|
||||
fpPitchRef.current = 0;
|
||||
|
||||
@@ -1376,6 +1596,9 @@ function switchToFirstPersonCamera() {
|
||||
playerRef.current.position.y + headHeight,
|
||||
playerRef.current.position.z
|
||||
);
|
||||
// Не большой сдвиг камеры вперёд, чтобы не упираться в скрытую голову
|
||||
const forward = new THREE.Vector3(0, 0, -0.08).applyEuler(new THREE.Euler(0, playerRef.current.rotation.y, 0));
|
||||
fpCamRef.current.position.add(forward);
|
||||
|
||||
// Направляем камеру в том же направлении, что и игрок
|
||||
const direction = new THREE.Vector3(0, 0, -1);
|
||||
@@ -1395,6 +1618,11 @@ function switchToThirdPersonCamera() {
|
||||
}
|
||||
if (playerRef.current) {
|
||||
playerRef.current.visible = true;
|
||||
// Вернуть видимость скрытых для FPV узлов
|
||||
if (Array.isArray(fpHiddenNodesRef.current)) {
|
||||
fpHiddenNodesRef.current.forEach(n => { n.visible = true; });
|
||||
fpHiddenNodesRef.current = [];
|
||||
}
|
||||
console.log('Игрок показан');
|
||||
}
|
||||
fpPitchRef.current = 0;
|
||||
@@ -1414,42 +1642,95 @@ function stopMove(dir) {
|
||||
// ─────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
const onClick = (e) => {
|
||||
console.log('[INTERIOR CLICK] handler start; isInInterior:', isInInteriorRef.current);
|
||||
if (!isInInteriorRef.current) return;
|
||||
const mount = mountRef.current;
|
||||
if (!mount || !cameraRef.current) return;
|
||||
|
||||
// координаты мыши в NDC
|
||||
const rect = mount.getBoundingClientRect();
|
||||
// Пытаемся получить координаты из элемента рендера (FP вид)
|
||||
const canvas = rendererRef.current && rendererRef.current.domElement;
|
||||
const rect = (canvas || mount).getBoundingClientRect();
|
||||
const mouse = new THREE.Vector2(
|
||||
((e.clientX - rect.left) / rect.width) * 2 - 1,
|
||||
-((e.clientY - rect.top) / rect.height) * 2 + 1
|
||||
);
|
||||
const raycaster = new THREE.Raycaster();
|
||||
raycaster.setFromCamera(mouse, cameraRef.current);
|
||||
// Ищем пересечения по интерактивам
|
||||
// Ищем пересечения по интерактивам (включая NPC)
|
||||
const objects = interiorInteractablesRef.current.filter(obj => obj?.isObject3D);
|
||||
if (!objects.length) return;
|
||||
const hits = raycaster.intersectObjects(objects, true)
|
||||
.filter(h => h.object && h.object.userData && h.object.userData.interactable);
|
||||
if (!hits.length) return;
|
||||
|
||||
// Добавим в список интерактивов саму группу интерьера, чтобы traverse детектил payload у вложенных узлов
|
||||
const extraTargets = [];
|
||||
if (interiorGroupRef.current) extraTargets.push(interiorGroupRef.current);
|
||||
const rayHits = raycaster.intersectObjects(objects.concat(extraTargets), true);
|
||||
console.log('[INTERIOR CLICK] rayHits count:', rayHits.length);
|
||||
const hits = rayHits.filter(h => {
|
||||
const obj = h.object;
|
||||
// учитываем payload на мешах и на родителях
|
||||
if (obj && obj.userData && (obj.userData.interactable || obj.userData.payload || obj.userData.isNpc)) return true;
|
||||
let p = obj;
|
||||
while (p && p.parent) {
|
||||
p = p.parent;
|
||||
if (p.userData && (p.userData.interactable || p.userData.payload || p.userData.isNpc)) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
console.log('[INTERIOR CLICK] interactable hits count:', hits.length);
|
||||
if (hits.length) {
|
||||
const top = hits[0].object;
|
||||
const payload = top.userData.payload || {};
|
||||
// Дальше делай что нужно: диалог, меню, действие и т.п.
|
||||
// поднимаем до узла, где лежит payload
|
||||
let node = top;
|
||||
while (node && !node.userData?.payload && node.parent) node = node.parent;
|
||||
let payload = (node && node.userData && node.userData.payload) || (top.userData.payload) || {};
|
||||
// Если у попавшего меша нет payload, но это часть NPC, поднимемся до isNpc
|
||||
if ((!payload || !payload.type) && node) {
|
||||
let p = node;
|
||||
while (p && !p.userData?.isNpc && p.parent) p = p.parent;
|
||||
if (p && p.userData?.npcId) {
|
||||
payload = { type: 'npc', id: p.userData.npcId };
|
||||
}
|
||||
}
|
||||
console.log('[INTERIOR CLICK] payload:', payload);
|
||||
if (payload.type === 'marker') {
|
||||
console.log('Нажат маркер:', payload);
|
||||
// например, открыть окно диалога/описания
|
||||
// setCurrentDialog(...); setShowDialog(true);
|
||||
} else if (payload.type === 'npc') {
|
||||
console.log('Нажат NPC:', payload);
|
||||
// loadDialog(payload.id) и т.п.
|
||||
try { if (payload.id) { loadDialog(payload.id); } } catch (_) {}
|
||||
} else {
|
||||
console.log('Интерактив:', payload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Если своих интерактивов не нашли, пробуем поймать NPC из общего массива npcMeshes
|
||||
try {
|
||||
const npcHit = raycaster.intersectObjects(npcMeshesRef.current || [], true);
|
||||
console.log('[INTERIOR CLICK] npcMeshes hits:', npcHit.length);
|
||||
if (npcHit.length) {
|
||||
let root = npcHit[0].object;
|
||||
while (root.parent && !root.userData?.isNpc) root = root.parent;
|
||||
if (root.userData && root.userData.npcId) {
|
||||
console.log('[INTERIOR CLICK] NPC root found:', root.userData.npcId);
|
||||
if (root.userData.npcId === 'Computer') {
|
||||
setShowMiniGame(true);
|
||||
setPasswordCorrect(false);
|
||||
setAudioUrl('/audio/firs.ogg');
|
||||
addSeregaComment('Ну чё, хакер, разберёшься?');
|
||||
} else {
|
||||
loadDialog(root.userData.npcId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[INTERIOR CLICK] npcMeshes raycast failed:', e);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('click', onClick);
|
||||
return () => window.removeEventListener('click', onClick);
|
||||
const target = rendererRef.current ? rendererRef.current.domElement : window;
|
||||
target.addEventListener('click', onClick);
|
||||
target.addEventListener('pointerdown', onClick);
|
||||
return () => { target.removeEventListener('click', onClick); target.removeEventListener('pointerdown', onClick); };
|
||||
}, []);
|
||||
|
||||
async function buyItem(key) {
|
||||
@@ -1492,9 +1773,6 @@ useEffect(() => {
|
||||
function toggleWorldVisibility(visible) {
|
||||
groundRef.current && (groundRef.current.visible = visible);
|
||||
cityMeshesRef.current.forEach(m => m.visible = visible);
|
||||
Object.values(remotePlayersRef.current).forEach(p => {
|
||||
if (p.model) p.model.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
function createInterior() {
|
||||
@@ -1758,7 +2036,11 @@ useEffect(() => {
|
||||
});
|
||||
}
|
||||
|
||||
async function addOtherPlayer(id, x, z, avatarURL, genderRemote = 'male', firstName = '', lastName = '') {
|
||||
async function addOtherPlayer(id, x, z, avatarURL, genderRemote = 'male', firstName = '', lastName = '', y = 0) {
|
||||
if (remotePlayers[id]) {
|
||||
// Уже есть — не пересоздаём
|
||||
return;
|
||||
}
|
||||
let model;
|
||||
try {
|
||||
if (!avatarURL) throw new Error('no avatarURL');
|
||||
@@ -1793,7 +2075,7 @@ useEffect(() => {
|
||||
);
|
||||
}
|
||||
model.scale.set(1, 1, 1);
|
||||
model.position.set(x, 0, z);
|
||||
model.position.set(x, y || 0, z);
|
||||
scene.add(model);
|
||||
|
||||
const fullname = `${firstName} ${lastName}`.trim();
|
||||
@@ -2088,26 +2370,45 @@ useEffect(() => {
|
||||
socket.on('connect', () => console.log('Socket connected, id=', socket.id));
|
||||
socket.on('currentPlayers', (players) => {
|
||||
console.log('currentPlayers', players);
|
||||
// Получаем cityId текущего игрока из профиля
|
||||
// Получаем профиль (только для ФИО/аватара)
|
||||
const myProfile = JSON.parse(sessionStorage.getItem('user_profile') || '{}');
|
||||
const myCityId = myProfile.last_city_id || 1;
|
||||
// Добавляем/обновляем игроков из пришедшего списка
|
||||
Object.keys(players).forEach(id => {
|
||||
if (id === socket.id) return;
|
||||
const { x, z, avatarURL, gender, firstName, lastName, cityId } = players[id];
|
||||
if (cityId && cityId !== myCityId) return; // показываем только игроков своего города
|
||||
addOtherPlayer(id, x, z, avatarURL, gender, firstName, lastName);
|
||||
const { x, y, z, avatarURL, gender, firstName, lastName } = players[id];
|
||||
if (!remotePlayers[id]) {
|
||||
addOtherPlayer(id, x, z, avatarURL, gender, firstName, lastName, y);
|
||||
}
|
||||
});
|
||||
// После получения списка игроков, отправляем newPlayer о себе
|
||||
// Удаляем тех, кого нет в актуальном списке (после входа/выхода из интерьера и т.п.)
|
||||
const validIds = new Set(Object.keys(players));
|
||||
Object.keys(remotePlayers).forEach((rid) => {
|
||||
if (rid === socket.id) return;
|
||||
if (!validIds.has(rid)) {
|
||||
if (remotePlayers[rid] && remotePlayers[rid].model) {
|
||||
scene.remove(remotePlayers[rid].model);
|
||||
}
|
||||
delete remotePlayers[rid];
|
||||
if (voiceIcons.current[rid]) delete voiceIcons.current[rid];
|
||||
cleanupVoiceConnection(rid);
|
||||
}
|
||||
});
|
||||
|
||||
// После получения списка игроков, отправляем newPlayer о себе ТОЛЬКО когда мы не в интерьере
|
||||
// Отправляем себя только если это первый коннект и ещё не отправляли
|
||||
if (!window.__newPlayerSentOnce) {
|
||||
const profile = myProfile;
|
||||
socket.emit('newPlayer', {
|
||||
x: player?.position?.x || 0,
|
||||
y: player?.position?.y || 0,
|
||||
z: player?.position?.z || 0,
|
||||
avatarURL: avatarUrl,
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
userId: profile.id,
|
||||
cityId: myCityId
|
||||
userId: profile.id
|
||||
});
|
||||
window.__newPlayerSentOnce = true;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('chatMessage', ({ playerId, name, message, position }) => {
|
||||
@@ -2149,7 +2450,7 @@ useEffect(() => {
|
||||
const remote = remotePlayers[data.playerId];
|
||||
if (!remote) return;
|
||||
|
||||
const newPos = new THREE.Vector3(data.x, 0, data.z);
|
||||
const newPos = new THREE.Vector3(data.x, typeof data.y === 'number' ? data.y : remote.model.position.y, data.z);
|
||||
const dir = new THREE.Vector3().subVectors(newPos, remote.model.position);
|
||||
if (dir.lengthSq() > 1e-4) {
|
||||
const angle = Math.atan2(dir.x, dir.z);
|
||||
@@ -2204,6 +2505,8 @@ useEffect(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Если мы сейчас внутри интерьера, показывать новых игроков следует только когда они тоже будут в нашем списке currentPlayers,
|
||||
// который уже фильтруется сервером по interiorId. Здесь просто добавляем как обычно.
|
||||
addOtherPlayer(playerId, x, z, avatarURL, gender, firstName, lastName);
|
||||
});
|
||||
|
||||
@@ -2256,6 +2559,7 @@ useEffect(() => {
|
||||
|
||||
function onMouseLookMove(e) {
|
||||
if (!isInInteriorRef.current || cameraRef.current !== fpCamRef.current || !playerRef.current) return;
|
||||
if (altHeldRef.current) return; // при зажатом Alt не вращаем камеру
|
||||
|
||||
// Throttling - обрабатываем только каждые 8ms (120fps для более плавного движения)
|
||||
if (mouseMoveTimeout) return;
|
||||
@@ -2402,22 +2706,7 @@ useEffect(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка событий pointer lock
|
||||
if (renderer && renderer.domElement) {
|
||||
renderer.domElement.addEventListener('click', () => {
|
||||
if (isInInteriorRef.current && !document.pointerLockElement) {
|
||||
renderer.domElement.requestPointerLock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('pointerlockchange', () => {
|
||||
if (renderer && renderer.domElement && document.pointerLockElement === renderer.domElement) {
|
||||
console.log('Pointer lock activated');
|
||||
} else {
|
||||
console.log('Pointer lock deactivated');
|
||||
}
|
||||
});
|
||||
// Pointer lock больше не используется в интерьере — курсор всегда активен
|
||||
|
||||
// Проверяем, что THREE.PlaneGeometry доступен
|
||||
if (!THREE.PlaneGeometry) {
|
||||
@@ -2540,7 +2829,7 @@ useEffect(() => {
|
||||
const npcData = [
|
||||
{ id: 'bartender', model: '/models/npc/bartender.glb', position: [0, 0, 10] },
|
||||
{ id: 'guard', model: '/models/npc/guard.glb', position: [0, 0, 5] },
|
||||
{ id: 'Adventurer', model: '/models/npc/Adventurer.glb', position: [0, 0, -5] },
|
||||
{ id: 'Adventurer', model: '/models/npc/galina.glb', position: [-16.5, -100, -68.8] },
|
||||
{ id: 'BeachCharacter', model: '/models/npc/BeachCharacter.glb', position: [0, 0, 3] },
|
||||
{ id: 'Oxranik', model: '/models/npc/Oxranik.glb', position: [0, 0, -3] },
|
||||
{ id: 'Computer', model: '/models/npc/Computer.glb', position: [0.1, 0.1, 2.1] }
|
||||
@@ -2605,6 +2894,7 @@ useEffect(() => {
|
||||
model.rotateY(Math.PI); // Развернуть персонажа
|
||||
scene.add(model);
|
||||
npcMeshes.push(model); // Правильное добавление в массив
|
||||
npcMeshesRef.current.push(model);
|
||||
cityMeshesRef.current.push(model);
|
||||
|
||||
if (npc.id == 'Computer') {
|
||||
@@ -2734,14 +3024,7 @@ useEffect(() => {
|
||||
|
||||
updateCameraFollow();
|
||||
|
||||
socketRef.current?.emit('newPlayer', {
|
||||
x: player.position.x,
|
||||
z: player.position.z,
|
||||
avatarURL: avatarUrl,
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
userId: profile.id
|
||||
});
|
||||
// Не отправляем здесь newPlayer — делаем это централизованно после currentPlayers
|
||||
} catch (err) {
|
||||
console.error("Ошибка загрузки модели игрока:", err);
|
||||
console.error("Детали ошибки:", {
|
||||
@@ -3148,15 +3431,11 @@ useEffect(() => {
|
||||
|
||||
function onKeyDown(event) {
|
||||
keys[event.key] = true;
|
||||
if (event.key === 'Alt') altHeldRef.current = true;
|
||||
|
||||
console.log('onKeyDown:', event.key, 'isInInteriorRef.current:', isInInteriorRef.current);
|
||||
|
||||
// Обработка клавиши Escape для выхода из интерьера
|
||||
if (event.key === 'Escape' && isInInteriorRef.current) {
|
||||
console.log('Escape нажата - выходим из интерьера');
|
||||
exitInterior();
|
||||
return;
|
||||
}
|
||||
// ESC больше не выходит из интерьера
|
||||
|
||||
if (isInInteriorRef.current) {
|
||||
console.log('Обрабатываем клавишу в интерьере:', event.key);
|
||||
@@ -3184,6 +3463,7 @@ useEffect(() => {
|
||||
|
||||
function onKeyUp(event) {
|
||||
keys[event.key] = false;
|
||||
if (event.key === 'Alt') altHeldRef.current = false;
|
||||
if (isInInteriorRef.current) {
|
||||
const k = event.key.toLowerCase();
|
||||
if (k === 'arrowup' || k === 'w') stopMove('forward');
|
||||
@@ -3349,7 +3629,7 @@ useEffect(() => {
|
||||
const angle = Math.atan2(dir.x, dir.z);
|
||||
const targetQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, angle, 0));
|
||||
player.quaternion.slerp(targetQuat, Math.min(1, 10 * delta));
|
||||
socketRef.current?.emit('playerMovement', { x: player.position.x, z: player.position.z });
|
||||
socketRef.current?.emit('playerMovement', { x: player.position.x, y: player.position.y, z: player.position.z });
|
||||
|
||||
if (currentAction !== walkAction) {
|
||||
currentAction.fadeOut(0.2);
|
||||
@@ -3480,19 +3760,53 @@ useEffect(() => {
|
||||
const speed = 2; // Уменьшаем скорость для более плавного движения в интерьере
|
||||
const rotSpeed = Math.PI * 0.5; // Уменьшаем скорость поворота
|
||||
|
||||
// Проверка триггера выхода по внутренней точке
|
||||
if (interiorExitPosRef.current && player.position.distanceTo(interiorExitPosRef.current) < 0.7) {
|
||||
exitInterior();
|
||||
return;
|
||||
}
|
||||
|
||||
// Поворот влево-вправо (A/D или стрелки)
|
||||
if (move.left) player.rotation.y += rotSpeed * delta;
|
||||
if (move.right) player.rotation.y -= rotSpeed * delta;
|
||||
// Камера следует за вращением тела
|
||||
const headHeight = 1.6;
|
||||
const camBase = new THREE.Vector3(player.position.x, player.position.y + headHeight, player.position.z);
|
||||
const camForward = new THREE.Vector3(0, 0, -0.08).applyEuler(new THREE.Euler(0, player.rotation.y, 0));
|
||||
fpCamRef.current.position.copy(camBase.add(camForward));
|
||||
const lookForward = new THREE.Vector3(0, 0, -1).applyEuler(new THREE.Euler(0, player.rotation.y, 0));
|
||||
fpCamRef.current.lookAt(fpCamRef.current.position.clone().add(lookForward));
|
||||
|
||||
// Движение с проверкой коллизий
|
||||
const tryMove = (dirVec) => {
|
||||
const candidate = player.position.clone().addScaledVector(dirVec, speed * delta);
|
||||
// Обновляем AABB игрока (простая капсула не используется, только коробка)
|
||||
const half = 0.3; // половина ширины
|
||||
const height = 1.8;
|
||||
const playerBox = new THREE.Box3(
|
||||
new THREE.Vector3(candidate.x - half, candidate.y, candidate.z - half),
|
||||
new THREE.Vector3(candidate.x + half, candidate.y + height, candidate.z + half)
|
||||
);
|
||||
const hits = (interiorCollidersRef.current || []).some((mesh) => {
|
||||
const box = new THREE.Box3().setFromObject(mesh);
|
||||
return box.intersectsBox(playerBox);
|
||||
});
|
||||
if (!hits) {
|
||||
player.position.copy(candidate);
|
||||
}
|
||||
};
|
||||
|
||||
// Движение вперед-назад (W/S или стрелки)
|
||||
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(player.quaternion);
|
||||
if (move.forward) player.position.addScaledVector(forward, speed * delta);
|
||||
if (move.backward) player.position.addScaledVector(forward, -speed * delta);
|
||||
|
||||
// Боковое движение (Q/E для strafe, если нужно)
|
||||
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(player.quaternion);
|
||||
if (move.strafeLeft) player.position.addScaledVector(right, -speed * delta);
|
||||
if (move.strafeRight) player.position.addScaledVector(right, speed * delta);
|
||||
if (move.forward) tryMove(forward);
|
||||
if (move.backward) tryMove(forward.clone().multiplyScalar(-1));
|
||||
if (move.strafeLeft) tryMove(right.clone().multiplyScalar(-1));
|
||||
if (move.strafeRight) tryMove(right);
|
||||
|
||||
// Отправляем позицию внутри интерьера, чтобы нас видели другие внутри
|
||||
if (socketRef.current) {
|
||||
socketRef.current.emit('playerMovement', { x: player.position.x, y: player.position.y, z: player.position.z });
|
||||
}
|
||||
}
|
||||
|
||||
function updateCameraFollow() {
|
||||
@@ -3549,7 +3863,8 @@ useEffect(() => {
|
||||
}
|
||||
|
||||
// Обновляем движение игрока
|
||||
if (typeof updateDestinationMovement === 'function') {
|
||||
// В интерьере отключаем автодвижение по кликам (двигаемся только WASD)
|
||||
if (!isInInteriorRef.current && typeof updateDestinationMovement === 'function') {
|
||||
updateDestinationMovement(delta);
|
||||
}
|
||||
if (typeof updateFirstPersonMovement === 'function') {
|
||||
|
||||
@@ -7,13 +7,13 @@ export const useDialogManager = () => {
|
||||
const [formData, setFormData] = useState({});
|
||||
const [currentForm, setCurrentForm] = useState(null);
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
const markDialogAsListened = async (jsonFilename) => {
|
||||
try {
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
const filename = jsonFilename.split('/').pop().split('\\').pop();
|
||||
console.log('Normalized filename:', filename);
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD>111<31>");
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD>111<31>");
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('/api/listen', {
|
||||
method: 'POST',
|
||||
@@ -22,17 +22,18 @@ export const useDialogManager = () => {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
player_id: JSON.parse(sessionStorage.getItem('user_profile')).email,
|
||||
// player_id больше не обязателен: сервер возьмёт его из токена/сессии при наличии
|
||||
json_filename: filename
|
||||
})
|
||||
});
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD><EFBFBD>3455654");
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD><EFBFBD>3455654");
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>');
|
||||
const txt = await response.text().catch(()=> '');
|
||||
console.error('Ошибка при записи прослушанного:', response.status, txt);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>:', error);
|
||||
console.error('Ошибка сети при записи прослушанного:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,25 +45,25 @@ export const useDialogManager = () => {
|
||||
setDialogIndex(0);
|
||||
setShowDialog(true);
|
||||
} catch (error) {
|
||||
console.error('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', error);
|
||||
console.error('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnswerSelect = async (answer) => {
|
||||
console.log('[Debug] Answer object:', answer); // <- <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?
|
||||
console.log('[Debug] "end" in answer:', 'end' in answer); // <- <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> end?
|
||||
console.log('[Debug] Answer object:', answer); // <- <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?
|
||||
console.log('[Debug] "end" in answer:', 'end' in answer); // <- <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> end?
|
||||
if (answer.end !== undefined) {
|
||||
console.log('[Debug] Dialog end triggered!');
|
||||
// <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
if (currentDialog?.filename) {
|
||||
await markDialogAsListened(currentDialog.filename);
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD><EFBFBD>");
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD><EFBFBD>");
|
||||
}
|
||||
setShowDialog(false);
|
||||
} else if (answer.next !== undefined) {
|
||||
if (typeof answer.next === 'string' && answer.next.startsWith('form_')) {
|
||||
const nextNode = currentDialog.dialog.find(node => node.id === answer.next);
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD><EFBFBD>, <20><> <20><><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD>");
|
||||
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD><EFBFBD>, <20><> <20><><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD>");
|
||||
if (nextNode && nextNode.type === 'form') {
|
||||
setCurrentForm(nextNode);
|
||||
return;
|
||||
@@ -73,12 +74,12 @@ export const useDialogManager = () => {
|
||||
if (nextIndex !== -1) {
|
||||
setDialogIndex(nextIndex);
|
||||
} else {
|
||||
console.error('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', answer.next);
|
||||
console.error('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', answer.next);
|
||||
setShowDialog(false);
|
||||
}
|
||||
}
|
||||
else if (answer.next == answer.end) {
|
||||
console.log("<22><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>");
|
||||
console.log("<22><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>");
|
||||
}
|
||||
else {
|
||||
setShowDialog(false);
|
||||
@@ -92,9 +93,9 @@ export const useDialogManager = () => {
|
||||
if (nextIndex !== -1) {
|
||||
setDialogIndex(nextIndex);
|
||||
setCurrentForm(null);
|
||||
console.log('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', formData);
|
||||
console.log('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', formData);
|
||||
|
||||
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <20><><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
const nextNode = currentDialog.dialog[nextIndex];
|
||||
if (nextNode.end && currentDialog?.filename) {
|
||||
await markDialogAsListened(currentDialog.filename);
|
||||
|
||||
Reference in New Issue
Block a user