Compare commits

...

10 Commits

61 changed files with 18247 additions and 5307 deletions

149
COLLIDER_CONFIGURATION.md Normal file
View File

@@ -0,0 +1,149 @@
# Конфигурация системы коллайдеров
## Обзор изменений
Система коллайдеров была обновлена для обеспечения полного покрытия объектов коллизиями. Теперь коллайдеры создаются с увеличенными размерами, чтобы покрывать весь объект, а не только его часть.
## Основные изменения
### 1. Конфигурация коллайдеров
```javascript
const COLLIDER_CONFIG = {
sizeMultiplier: 2.0, // Коэффициент увеличения размеров (100% больше)
debugMode: false, // Режим отладки для визуализации
minSize: 0.5, // Минимальный размер коллайдера
maxSize: 50.0, // Максимальный размер коллайдера
adaptiveScaling: true // Адаптивное масштабирование
};
```
### 2. Обновленная функция создания коллайдеров
- `createVisualColliderFromModel()` теперь использует увеличенные размеры для JSON данных
- Приоритет отдается реальным размерам мешей из 3D модели
- Fallback к JSON данным с коэффициентом увеличения
### 3. Адаптивное масштабирование
Система автоматически определяет оптимальный коэффициент масштабирования на основе размеров объекта:
- **Маленькие объекты** (< 1.0): коэффициент 3.0x или больше
- **Средние объекты** (1.0-5.0): стандартный коэффициент 2.0x
- **Большие объекты** (> 5.0): уменьшенный коэффициент 1.6x
### 4. Динамическая настройка
Добавлены расширенные функции для настройки коллайдеров:
```javascript
// Изменить коэффициент размера коллайдеров
window.updateColliderSize(3.0); // Увеличить на 200%
// Переключить адаптивное масштабирование
window.toggleAdaptiveScaling();
// Установить ограничения размеров
window.setColliderLimits(1.0, 20.0); // мин: 1.0, макс: 20.0
// Переключить режим отладки
window.toggleColliderDebug();
// Получить текущую конфигурацию
console.log(window.colliderConfig);
// Протестировать коллизии
window.testCollisions();
// Быстрый тест всех функций
window.quickTest();
// Управление цветом и прозрачностью коллайдеров
window.setColliderColor(0, 1, 0); // Зеленый цвет (RGB 0-1)
window.setColliderOpacity(0.5); // Полупрозрачность (0-1)
window.randomizeColliderColors(); // Случайные цвета
// Управление цветом и прозрачностью объектов интерьера
window.setInteriorObjectColor(1, 0, 0); // Красный цвет объектов (RGB 0-1)
window.setInteriorObjectOpacity(0.7); // Полупрозрачность объектов (0-1)
```
## Использование
### Базовое использование
Коллайдеры автоматически загружаются из `colliders_city_1.json` с увеличенными размерами.
### Настройка размеров
Если коллайдеры слишком большие или маленькие, можно изменить коэффициент:
```javascript
// В консоли браузера
window.updateColliderSize(1.0); // Оригинальные размеры
window.updateColliderSize(1.2); // +20% (по умолчанию)
window.updateColliderSize(1.5); // +50%
```
### Отладка
Для визуализации коллайдеров включите режим отладки:
```javascript
window.colliderConfig.debugMode = true;
```
## Технические детали
### Алгоритм создания коллайдеров
1. Поиск соответствующего меша в 3D модели по позиции
2. Если меш найден - использование его реальных размеров
3. Если меш не найден - использование JSON данных с коэффициентом увеличения
4. Создание Box3 для проверки коллизий
5. Создание визуального представления
### Проверка коллизий
- Используется AABB (Axis-Aligned Bounding Box) для эффективной проверки
- Игрок представлен как цилиндр с радиусом 0.35 и высотой 1.6
- Проверка пересечения Box3 объектов
## Файлы конфигурации
### colliders_city_1.json
```json
{
"colliders": [
{
"type": "box",
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 2, "y": 2, "z": 2 },
"color": { "r": 1.0, "g": 0.0, "b": 0.0 },
"opacity": 0.3
}
]
}
```
**Параметры:**
- `position`, `rotation`, `scale` - стандартные параметры трансформации
- `color` - RGB цвет в диапазоне 0.0-1.0 (опционально)
- `opacity` - прозрачность в диапазоне 0.0-1.0 (опционально)
Размеры в JSON будут автоматически увеличены на коэффициент `sizeMultiplier`.
## Рекомендации
1. **Начальная настройка**: Используйте коэффициент 1.2 (20% увеличение)
2. **Тонкая настройка**: Тестируйте разные значения от 1.0 до 1.5
3. **Отладка**: Включите `debugMode` для визуальной проверки коллайдеров
4. **Производительность**: Большие коэффициенты могут снизить производительность
## Устранение неполадок
### Коллайдеры слишком маленькие
```javascript
window.updateColliderSize(1.3); // Увеличить коэффициент
```
### Коллайдеры слишком большие
```javascript
window.updateColliderSize(1.1); // Уменьшить коэффициент
```
### Проблемы с производительностью
```javascript
window.updateColliderSize(1.0); // Вернуться к оригинальным размерам
```

317
MIGRATION_GUIDE.md Normal file
View File

@@ -0,0 +1,317 @@
# Руководство по миграции на модульную архитектуру
## Обзор
Этот документ описывает процесс миграции проекта EEV_Proj с монолитной архитектуры на модульную. Миграция направлена на улучшение читаемости, поддерживаемости и переиспользования кода.
## Что было изменено
### До миграции
- Весь код игры находился в одном файле `Game.js` (6271 строк)
- Смешанная логика: 3D сцена, UI, бизнес-логика, API вызовы
- Сложность отладки и внесения изменений
- Низкая переиспользуемость компонентов
### После миграции
- Код разбит на логические модули
- Четкое разделение ответственности
- Легкость тестирования и отладки
- Возможность переиспользования модулей
## Структура модулей
### 1. Core (Ядро)
```
src/modules/
├── GameCore.js # Основной игровой движок
├── SceneManager.js # Управление 3D сценой
├── CameraManager.js # Управление камерами
├── PlayerManager.js # Управление игроком
├── RendererManager.js # Управление рендерером
└── InteriorManager.js # Управление интерьерами
```
### 2. Дополнительные модули (планируются)
```
src/modules/
├── DialogManager.js # Система диалогов
├── InventoryManager.js # Управление инвентарем
├── QuestManager.js # Система квестов
├── PhoneManager.js # Виртуальный телефон
├── AppManager.js # Управление приложениями
├── NotificationManager.js # Система уведомлений
├── SocketManager.js # WebSocket соединения
├── VoiceChatManager.js # Голосовой чат
├── MessageManager.js # Система сообщений
├── Pathfinding.js # Поиск пути
├── CollisionDetection.js # Обнаружение коллизий
└── AnimationManager.js # Управление анимациями
```
## Процесс миграции
### Шаг 1: Создание модулей
1. **SceneManager.js** - Выделен из логики управления сценой
2. **CameraManager.js** - Выделен из логики управления камерами
3. **PlayerManager.js** - Выделен из логики управления игроком
4. **RendererManager.js** - Выделен из логики рендеринга
5. **InteriorManager.js** - Выделен из логики интерьеров
6. **GameCore.js** - Создан как координатор всех модулей
### Шаг 2: Обновление Game.js
- Создан `GameModular.js` с использованием модулей
- Упрощена логика компонента
- Улучшена читаемость кода
### Шаг 3: Создание документации
- README.md для каждого модуля
- Общая документация проекта
- Руководства по использованию
## Как использовать новые модули
### Импорт модулей
```javascript
import { GameCore } from './modules/GameCore.js';
import { SceneManager } from './modules/SceneManager.js';
import { CameraManager } from './modules/CameraManager.js';
```
### Создание экземпляра игры
```javascript
const gameCore = new GameCore(containerElement);
```
### Доступ к модулям
```javascript
const sceneManager = gameCore.getSceneManager();
const cameraManager = gameCore.getCameraManager();
const playerManager = gameCore.getPlayerManager();
```
## Преимущества новой архитектуры
### 1. Читаемость
- Каждый модуль отвечает за одну область
- Код легче понимать и анализировать
- Упрощена отладка
### 2. Поддерживаемость
- Изменения в одном модуле не влияют на другие
- Легче добавлять новую функциональность
- Проще исправлять ошибки
### 3. Переиспользование
- Модули можно использовать в других частях проекта
- Возможность создания библиотеки модулей
- Легкость интеграции в новые проекты
### 4. Тестирование
- Каждый модуль можно тестировать отдельно
- Упрощено создание unit тестов
- Лучшее покрытие кода
### 5. Масштабируемость
- Легко добавлять новые модули
- Возможность параллельной разработки
- Лучшая организация команды
## Планы по дальнейшему развитию
### Краткосрочные цели (1-2 недели)
1. Завершить миграцию основных компонентов
2. Добавить недостающие модули
3. Создать полную документацию
4. Написать unit тесты для модулей
### Среднесрочные цели (1-2 месяца)
1. Добавить TypeScript поддержку
2. Создать систему плагинов
3. Реализовать hot reload для модулей
4. Добавить систему логирования
### Долгосрочные цели (3-6 месяцев)
1. Создать редактор модулей
2. Реализовать систему версионирования
3. Добавить поддержку WebAssembly
4. Создать marketplace модулей
## Обратная совместимость
### Что работает как раньше
- Основной API игры
- React компоненты
- WebSocket соединения
- API вызовы
### Что изменилось
- Внутренняя архитектура
- Структура файлов
- Способ инициализации игры
### Миграция существующего кода
```javascript
// Старый способ
import Game from './Game.js';
// Новый способ
import Game from './GameModular.js';
// или
import { GameCore } from './modules/GameCore.js';
```
## Проблемы и решения
### Проблема: Циклические зависимости
**Решение:** Правильное планирование архитектуры модулей
### Проблема: Производительность
**Решение:** Ленивая загрузка модулей, оптимизация импортов
### Проблема: Размер бандла
**Решение:** Tree shaking, динамические импорты
### Проблема: Отладка
**Решение:** Улучшенное логирование, source maps
## Рекомендации по разработке
### 1. Создание новых модулей
- Следуйте принципам SOLID
- Используйте JSDoc для документирования
- Добавляйте unit тесты
- Следуйте принятым соглашениям
### 2. Изменение существующих модулей
- Не нарушайте публичный API
- Обновляйте документацию
- Добавляйте тесты для новых функций
- Проверяйте обратную совместимость
### 3. Интеграция модулей
- Используйте события для слабой связанности
- Избегайте прямых зависимостей
- Создавайте четкие интерфейсы
- Документируйте взаимодействие
## Тестирование
### Unit тесты
```bash
npm test
```
### Интеграционные тесты
```bash
npm run test:integration
```
### E2E тесты
```bash
npm run test:e2e
```
## Отладка
### Логирование
```javascript
// Включение подробного логирования
localStorage.setItem('debug', 'true');
// Логирование конкретного модуля
localStorage.setItem('debug', 'SceneManager,CameraManager');
```
### DevTools
- React DevTools для компонентов
- Three.js Inspector для 3D сцены
- Chrome DevTools для отладки
## Производительность
### Оптимизации
1. Ленивая загрузка модулей
2. Кэширование результатов
3. Оптимизация рендеринга
4. Сжатие ресурсов
### Мониторинг
```javascript
// Включение профилирования
localStorage.setItem('profile', 'true');
// Метрики производительности
const metrics = gameCore.getPerformanceMetrics();
console.log('FPS:', metrics.fps);
console.log('Memory:', metrics.memory);
```
## Безопасность
### Валидация входных данных
- Проверка типов
- Санитизация строк
- Валидация координат
### Защита от эксплойтов
- CSP заголовки
- Валидация WebSocket сообщений
- Проверка прав доступа
## Развертывание
### Сборка
```bash
npm run build
```
### Анализ бандла
```bash
npm run analyze
```
### Оптимизация
```bash
npm run optimize
```
## Поддержка
### Документация
- README файлы для каждого модуля
- Примеры использования
- API документация
- Руководства по миграции
### Сообщество
- Issues на GitHub
- Обсуждения в Discord
- Wiki проекта
- Примеры кода
## Заключение
Миграция на модульную архитектуру значительно улучшает качество кода проекта EEV_Proj. Новый подход обеспечивает:
- Лучшую организацию кода
- Упрощенную разработку
- Повышенную надежность
- Возможности для роста
Процесс миграции выполнен поэтапно, что минимизирует риски и обеспечивает плавный переход. Все существующие функции сохранены, а новые возможности легко добавляются через модульную систему.
## Следующие шаги
1. Изучите документацию модулей
2. Попробуйте новый `GameModular.js`
3. Создайте свой первый модуль
4. Внесите вклад в развитие проекта
Удачи в разработке! 🚀

60
daily_availability.py Normal file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
# daily_availability.py
import logging
from datetime import date
import httpx
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
from config import *
# ====== CONFIGURATION ======
API_BASE = "http://127.0.0.1:8000"
TIMEOUT = httpx.Timeout(connect=5.0, read=30.0, write=5.0, pool=5.0)
# ====== LOGGING SETUP ======
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s | %(levelname)s | %(message)s',
handlers=[logging.FileHandler("daily_availability.log", encoding="utf-8")]
)
logger = logging.getLogger(__name__)
# ====== JOB FUNCTION ======
def check_availability():
today = date.today().isoformat()
url = f"{API_BASE}/availability_group"
headers = {
"X-API-KEY": API_KEY,
"Content-Type": "application/json" # хотя httpx сам проставит, но можно явно
}
payload = {"dates": [today]}
logger.info(f"Запрос создания доступности групп на {today}")
try:
with httpx.Client(timeout=TIMEOUT) as client:
response = client.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
logger.info(f"Успешно создана доступность: {data}")
except httpx.HTTPStatusError as e:
allow = e.response.headers.get("Allow")
logger.error(f"Метод не разрешён (status={e.response.status_code}), Allow={allow}")
except Exception as e:
logger.error(f"Ошибка при запросе доступности: {e}")
# ====== SCHEDULER SETUP ======
if __name__ == "__main__":
scheduler = BlockingScheduler(timezone="Asia/Yerevan")
# Запускаем задачу каждый день в 00:01
scheduler.add_job(
check_availability,
trigger=CronTrigger(hour=0, minute=1),
name="daily_availability_check"
)
logger.info("Scheduler запущен. Ожидание выполнения задачи в 00:01...")
try:
scheduler.start()
except (KeyboardInterrupt, SystemExit):
logger.info("Scheduler остановлен пользователем")

15
db1.js
View File

@@ -2,15 +2,22 @@ require('dotenv').config();
const { Pool } = require('pg'); const { Pool } = require('pg');
const connectionString = const connectionString =
process.env.DATABASE_URL_VIRTUAL_WORLD || process.env.DATABASE_URL; process.env.DATABASE_URL_VIRTUAL_WORLD;
console.log('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: ', connectionString);
const virtualWorldPool = new Pool({ const virtualWorldPool = new Pool({
connectionString, connectionString,
ssl: false ssl: false
}); });
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
virtualWorldPool.on('error', (err) => {
console.error('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', err);
});
virtualWorldPool.on('connect', () => {
console.log('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>');
});
module.exports = { module.exports = {
virtualWorldPool: {
query: (text, params) => virtualWorldPool.query(text, params) query: (text, params) => virtualWorldPool.query(text, params)
}
}; };

48
db2.js Normal file
View File

@@ -0,0 +1,48 @@
require('dotenv').config();
const { Pool } = require('pg');
// Проверяем наличие строки подключения
if (!process.env.DATABASE_QUEST_NEW_QUESTS) {
console.error('❌ Ошибка: DATABASE_QUEST_NEW_QUESTS не задана в .env файле');
throw new Error('DATABASE_QUEST_NEW_QUESTS environment variable is required');
}
const connectionStr = process.env.DATABASE_QUEST_NEW_QUESTS;
console.log('Подключение к базе данных new_quests');
// Проверяем, содержит ли строка подключения пароль
if (!connectionStr.includes(':')) {
console.error('❌ Ошибка: Строка подключения не содержит пароль');
throw new Error('Database connection string must include password');
}
const new_quest_Base = new Pool({
connectionString: connectionStr,
ssl: false
});
// Обработчики событий
new_quest_Base.on('error', (err) => {
console.error('❌ Ошибка подключения к базе данных new_quests:', err.message);
});
new_quest_Base.on('connect', () => {
console.log('✅ Успешное подключение к базе данных new_quests');
});
// Функция для проверки подключения
new_quest_Base.testConnection = async () => {
try {
await new_quest_Base.query('SELECT 1 as test');
console.log('✅ Тест подключения к new_quests успешен');
return true;
} catch (error) {
console.error('❌ Тест подключения к new_quests failed:', error.message);
return false;
}
};
module.exports = {
query: (text, params) => new_quest_Base.query(text, params),
testConnection: () => new_quest_Base.testConnection()
};

60
migrate-colliders.js Normal file
View File

@@ -0,0 +1,60 @@
// Скрипт для выполнения миграции таблицы colliders
// Файл: migrate-colliders.js
const fs = require('fs');
const path = require('path');
const { query } = require('./db');
async function runMigration() {
try {
console.log('🚀 Запуск миграции для создания таблицы colliders...');
// Читаем SQL файл миграции
const migrationPath = path.join(__dirname, 'migrations', 'create_colliders_table.sql');
const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
// Выполняем миграцию
await query(migrationSQL);
console.log('✅ Миграция успешно выполнена!');
console.log('📊 Таблица colliders создана');
// Проверяем, что таблица создана
const result = await query(`
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'colliders'
ORDER BY ordinal_position
`);
console.log('📋 Структура таблицы colliders:');
result.rows.forEach(row => {
console.log(` - ${row.column_name}: ${row.data_type} (${row.is_nullable === 'YES' ? 'nullable' : 'not null'})`);
});
// Проверяем индексы
const indexes = await query(`
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'colliders'
`);
console.log('🔍 Индексы:');
indexes.rows.forEach(row => {
console.log(` - ${row.indexname}`);
});
} catch (error) {
console.error('❌ Ошибка при выполнении миграции:', error);
process.exit(1);
}
}
// Запускаем миграцию
runMigration().then(() => {
console.log('🎉 Миграция завершена успешно!');
process.exit(0);
}).catch(error => {
console.error('💥 Критическая ошибка:', error);
process.exit(1);
});

113
migrate-json-to-db.js Normal file
View File

@@ -0,0 +1,113 @@
// Скрипт для миграции коллайдеров из JSON в базу данных
// Файл: migrate-json-to-db.js
const fs = require('fs');
const path = require('path');
const { query } = require('./db');
async function migrateJsonToDb() {
try {
console.log('🚀 Запуск миграции коллайдеров из JSON в базу данных...');
// Читаем JSON файл
const jsonPath = path.join(__dirname, 'public', 'colliders_city_1.json');
if (!fs.existsSync(jsonPath)) {
console.log('❌ JSON файл не найден:', jsonPath);
return;
}
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
const data = JSON.parse(jsonContent);
if (!data.colliders || !Array.isArray(data.colliders)) {
console.log('❌ Неверный формат JSON файла');
return;
}
console.log(`📊 Найдено ${data.colliders.length} коллайдеров в JSON файле`);
// Начинаем транзакцию
await query('BEGIN');
// Очищаем существующие коллайдеры для города 1
await query('DELETE FROM colliders WHERE city_id = $1', [1]);
console.log('🗑️ Очищены существующие коллайдеры для города 1');
// Вставляем коллайдеры из JSON
let insertedCount = 0;
for (const collider of data.colliders) {
try {
await 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
)
`, [
1, // city_id
collider.type || 'box',
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
]);
insertedCount++;
console.log(`✅ Коллайдер ${insertedCount} мигрирован:`, {
type: collider.type,
position: collider.position,
color: collider.color
});
} catch (error) {
console.error(`❌ Ошибка при миграции коллайдера ${insertedCount + 1}:`, error);
throw error;
}
}
// Подтверждаем транзакцию
await query('COMMIT');
console.log(`🎉 Миграция завершена успешно!`);
console.log(`📊 Мигрировано ${insertedCount} коллайдеров из JSON в базу данных`);
// Проверяем результат
const result = await query('SELECT COUNT(*) as count FROM colliders WHERE city_id = $1', [1]);
console.log(`🔍 Проверка: в БД теперь ${result.rows[0].count} коллайдеров для города 1`);
} catch (error) {
// Откатываем транзакцию в случае ошибки
await query('ROLLBACK');
console.error('💥 Ошибка при миграции:', error);
process.exit(1);
}
}
// Запускаем миграцию
migrateJsonToDb().then(() => {
console.log('🚀 Миграция JSON -> БД завершена!');
process.exit(0);
}).catch(error => {
console.error('💥 Критическая ошибка:', error);
process.exit(1);
});

View File

@@ -0,0 +1,79 @@
-- Миграция для создания таблицы colliders
-- Файл: migrations/create_colliders_table.sql
-- Создание таблицы colliders
CREATE TABLE IF NOT EXISTS colliders (
id SERIAL PRIMARY KEY,
city_id INTEGER NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('box', 'circle', 'capsule')),
-- Позиция
position_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
position_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
position_z DECIMAL(15, 6) NOT NULL DEFAULT 0,
-- Поворот (в радианах)
rotation_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
rotation_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
rotation_z DECIMAL(15, 6) NOT NULL DEFAULT 0,
-- Масштаб
scale_x DECIMAL(15, 6) NOT NULL DEFAULT 1,
scale_y DECIMAL(15, 6) NOT NULL DEFAULT 1,
scale_z DECIMAL(15, 6) NOT NULL DEFAULT 1,
-- Цвет (RGB 0-1)
color_r DECIMAL(3, 2) NOT NULL DEFAULT 1.0 CHECK (color_r >= 0 AND color_r <= 1),
color_g DECIMAL(3, 2) NOT NULL DEFAULT 0.0 CHECK (color_g >= 0 AND color_g <= 1),
color_b DECIMAL(3, 2) NOT NULL DEFAULT 0.0 CHECK (color_b >= 0 AND color_b <= 1),
-- Прозрачность (0-1)
opacity DECIMAL(3, 2) NOT NULL DEFAULT 0.3 CHECK (opacity >= 0 AND opacity <= 1),
-- Метаданные
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-- Индексы для быстрого поиска
CONSTRAINT fk_colliders_city FOREIGN KEY (city_id) REFERENCES cities(id) ON DELETE CASCADE
);
-- Создание индексов для оптимизации запросов
CREATE INDEX IF NOT EXISTS idx_colliders_city_id ON colliders(city_id);
CREATE INDEX IF NOT EXISTS idx_colliders_type ON colliders(type);
CREATE INDEX IF NOT EXISTS idx_colliders_position ON colliders(position_x, position_y, position_z);
-- Создание функции для автоматического обновления updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Создание триггера для автоматического обновления updated_at
CREATE TRIGGER update_colliders_updated_at
BEFORE UPDATE ON colliders
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Комментарии к таблице и колонкам
COMMENT ON TABLE colliders IS 'Таблица коллайдеров для городов';
COMMENT ON COLUMN colliders.city_id IS 'ID города, к которому принадлежит коллайдер';
COMMENT ON COLUMN colliders.type IS 'Тип коллайдера: box, circle, capsule';
COMMENT ON COLUMN colliders.position_x IS 'X координата позиции';
COMMENT ON COLUMN colliders.position_y IS 'Y координата позиции';
COMMENT ON COLUMN colliders.position_z IS 'Z координата позиции';
COMMENT ON COLUMN colliders.rotation_x IS 'X компонент поворота (радианы)';
COMMENT ON COLUMN colliders.rotation_y IS 'Y компонент поворота (радианы)';
COMMENT ON COLUMN colliders.rotation_z IS 'Z компонент поворота (радианы)';
COMMENT ON COLUMN colliders.scale_x IS 'X компонент масштаба';
COMMENT ON COLUMN colliders.scale_y IS 'Y компонент масштаба';
COMMENT ON COLUMN colliders.scale_z IS 'Z компонент масштаба';
COMMENT ON COLUMN colliders.color_r IS 'Красный компонент цвета (0-1)';
COMMENT ON COLUMN colliders.color_g IS 'Зеленый компонент цвета (0-1)';
COMMENT ON COLUMN colliders.color_b IS 'Синий компонент цвета (0-1)';
COMMENT ON COLUMN colliders.opacity IS 'Прозрачность (0-1)';
COMMENT ON COLUMN colliders.created_at IS 'Время создания записи';
COMMENT ON COLUMN colliders.updated_at IS 'Время последнего обновления записи';

View File

@@ -1,14 +0,0 @@
warning: in the working copy of '.env', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.gitignore', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'db.js', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'db1.js', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'package-lock.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'server.js', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'src/Game.js', LF will be replaced by CRLF the next time Git touches it
.env | 4 ++--
db.js | 4 +---
db1.js | 2 +-
package-lock.json | 57 +++++++++++++++++++++++++++++--------------------------
server.js | 10 ++++------
src/Game.js | 40 +++++++++++++-------------------------
6 files changed, 51 insertions(+), 66 deletions(-)

196
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@readyplayerme/visage": "^6.10.0", "@readyplayerme/visage": "^6.10.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"compression": "^1.7.4", "compression": "^1.7.4",
"concurrently": "^8.2.2",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0", "express": "^5.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@@ -5841,6 +5842,84 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
}, },
"node_modules/concurrently": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
"integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
"dependencies": {
"chalk": "^4.1.2",
"date-fns": "^2.30.0",
"lodash": "^4.17.21",
"rxjs": "^7.8.1",
"shell-quote": "^1.8.1",
"spawn-command": "0.0.2",
"supports-color": "^8.1.1",
"tree-kill": "^1.2.2",
"yargs": "^17.7.2"
},
"bin": {
"conc": "dist/bin/concurrently.js",
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": "^14.13.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/concurrently/node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/concurrently/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/concurrently/node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/concurrently/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"engines": {
"node": ">=12"
}
},
"node_modules/confusing-browser-globals": { "node_modules/confusing-browser-globals": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@@ -6446,6 +6525,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -14618,6 +14712,14 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-array-concat": { "node_modules/safe-array-concat": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -15417,6 +15519,11 @@
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"deprecated": "Please use @jridgewell/sourcemap-codec instead" "deprecated": "Please use @jridgewell/sourcemap-codec instead"
}, },
"node_modules/spawn-command": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="
},
"node_modules/spdy": { "node_modules/spdy": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
@@ -16479,6 +16586,14 @@
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
}, },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/troika-three-text": { "node_modules/troika-three-text": {
"version": "0.49.1", "version": "0.49.1",
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz", "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz",
@@ -22270,6 +22385,61 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
}, },
"concurrently": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
"integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
"requires": {
"chalk": "^4.1.2",
"date-fns": "^2.30.0",
"lodash": "^4.17.21",
"rxjs": "^7.8.1",
"shell-quote": "^1.8.1",
"spawn-command": "0.0.2",
"supports-color": "^8.1.1",
"tree-kill": "^1.2.2",
"yargs": "^17.7.2"
},
"dependencies": {
"cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
}
},
"supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"requires": {
"has-flag": "^4.0.0"
}
},
"yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"requires": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
}
},
"yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
}
}
},
"confusing-browser-globals": { "confusing-browser-globals": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@@ -22667,6 +22837,14 @@
"is-data-view": "^1.0.1" "is-data-view": "^1.0.1"
} }
}, },
"date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"requires": {
"@babel/runtime": "^7.21.0"
}
},
"debug": { "debug": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -28343,6 +28521,14 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"requires": {
"tslib": "^2.1.0"
}
},
"safe-array-concat": { "safe-array-concat": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -28904,6 +29090,11 @@
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
}, },
"spawn-command": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="
},
"spdy": { "spdy": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
@@ -29704,6 +29895,11 @@
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
}, },
"tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
},
"troika-three-text": { "troika-three-text": {
"version": "0.49.1", "version": "0.49.1",
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz", "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz",

3
public/colliders.json Normal file
View File

@@ -0,0 +1,3 @@
{
"colliders": []
}

View File

@@ -0,0 +1,6 @@
{
"colliders": [
{
}
]
}

View File

@@ -1,150 +1,30 @@
{ {
"name": "Галина", "name": "Искатель приключений",
"avatar": "/images/npc/bartender.jpg", "avatar": "/images/npc/adventurer.jpg",
"filename": "Adventurer.json", "filename": "Adventurer.json",
"npc_id": 3,
"dialogue_key": "adventurer/advice",
"dialog": [ "dialog": [
{ {
"id": 0, "id": 0,
"text": "Ну? Значит, ты к нам за работой припёрся? Ладно, давай документы...", "text": "А, новое лицо! Ищешь работу? Я бы на твоем месте сначала поговорил со стражником у входа. Он всем новичкам дает наводки.",
"answers": [ "answers": [
{ {
"text": "Документов нет", "text": "Спасибо за совет",
"next": 1
},
{
"text": "Я просто осматриваюсь...",
"next": 2
},
{
"text": "...",
"end": true "end": true
},
{
"text": "А что ты знаешь о работе?",
"next": 1
} }
] ]
}, },
{ {
"id": 1, "id": 1,
"text": "Ах, у тебя их нет? Я бы удивилась другому развитию событий. Тогда придется заполнить их. Писать то хоть умеешь?", "text": "Я-то? Да много чего... Но сначала пройди базовый инструктаж. Без этого тебя никто серьезный не возьмет.",
"answers": [
{
"text": "Как-нибудь справлюсь",
"next": "form_data"
},
{
"text": "Может, есть работа без документов?",
"next": 3
}
]
},
{
"id": 2,
"text": "Осматриваешься? Ну-ну... Только не задерживайся слишком долго. В нашем городе без дела болтаться — себе дороже.",
"answers": [
{
"text": "А куда можно пойти?",
"next": 3
},
{
"text": "Ладно, тогда давай документы...",
"next": 1
}
]
},
{
"id": 3,
"text": "Слушай, есть один парень... сидит в кустах за углом. Зовут его Костя Ключник. Он как раз набирает людей для одной работы. Только не говори, что я тебя направила.",
"answers": [
{
"text": "Спасибо, попробую",
"next": 4
},
{
"text": "А что за работа?",
"next": 5
}
]
},
{
"id": 4,
"text": "Ну иди уже, не задерживайся. И смотри в оба — Костя любит шутки, но не над собой.",
"answers": [
{
"end": true
}
]
},
{
"id": 5,
"text": "Если бы я знала все детали, сама бы там работала. Иди сам узнаешь. Если, конечно, не струсишь.",
"answers": [
{
"text": "Ладно, иду",
"next": 4
},
{
"text": "Может, лучше заполню документы?",
"next": "form_data"
}
]
},
{
"id": "form_data",
"type": "form",
"title": "Заполнение анкеты",
"fields": [
{
"name": "skills",
"label": "Твои навыки и перки:",
"type": "text",
"placeholder": "Навыки через запятую",
"required": true
},
{
"name": "work_experience",
"label": "Предыдущие места работы:",
"type": "textarea",
"placeholder": "Где и кем работал",
"required": false
},
{
"name": "background",
"label": "Твое прошлое:",
"type": "textarea",
"placeholder": "Краткая информация о себе",
"required": false
}
],
"submit_text": "Отправить данные",
"next": 6
},
{
"id": 6,
"text": "Так... посмотрим что тут... (долго листает бумаги) Ну ты и говно, дружок. Ладно, есть для тебя один вариант.",
"answers": [
{
"text": "Какой?",
"next": 7
}
]
},
{
"id": 7,
"text": "Слушай сюда. Есть один парень... сидит в кустах за углом. Зовут его Костя Ключник. Он как раз набирает людей. Вот и вся вакансия.",
"answers": [
{
"text": "И это всё?",
"next": 8
},
{
"text": "Ладно, пойду",
"next": 4
}
]
},
{
"id": 8,
"text": "Да, это всё. Ты думал, у нас тут офис белых воротничков? Иди уже, не задерживайся.",
"answers": [ "answers": [
{ {
"text": "Понял, спасибо",
"end": true "end": true
} }
] ]

View File

@@ -1,167 +1,47 @@
{ {
"name": "Костя Ключник", "name": "Пляжный персонаж",
"avatar": "/images/npc/guard.jpg", "avatar": "/images/npc/beach_character.jpg",
"filename": "BeachCharacter.json", "filename": "BeachCharacter.json",
"npc_id": 4,
"dialogue_key": "beachcharacter/meet",
"dialog": [ "dialog": [
{ {
"id": 0, "id": 0,
"text": "(нервно озираясь по сторонам) Тссс... Ты кто такой? Мент? А? Нет? Ну ладно... (быстро чешет нос) Галина говорила? Ну эта... которая в баре...", "text": "Эй, приятель! Ты выглядишь как человек, который ищет приключений. У меня есть одно дельце...",
"answers": [ "answers": [
{ {
"text": "Да, она направила", "text": "Какое дело?",
"next": 1 "next": 1
}, },
{ {
"text": "Кто такая Галина?", "text": "Извини, я спешу",
"next": 2 "end": true
} }
] ]
}, },
{ {
"id": 1, "id": 1,
"text": "(хихикает) Ага, ну конечно направила... Она у нас вся такая... (внезапно серьёзнеет) Ладно, браток, работа есть. Но сначала... (ковыряет в носу) Ты че, стремаешься?", "text": "Нужно кое-что доставить в старую часть города. Опасно, но платят хорошо. Заинтересован?",
"answers": [ "answers": [
{ {
"text": "Какая работа?", "text": "Расскажи подробнее",
"next": 3 "next": 2
}, },
{ {
"text": "Ты че такой странный?", "text": "Слишком рискованно",
"next": 4 "end": true
} }
] ]
}, },
{ {
"id": 2, "id": 2,
"text": "(параноидально оглядывается) Ты че, прикалываешься? Галина! Ну... (делает жест рукой у рта) Которая... Ну в общем... (внезапно меняет тему) Ты хочешь заработать или нет?", "text": "Встреться с моим контактом у заброшенного дока. Скажешь, что от Кости. Деньги получишь по завершению.",
"answers": [
{
"text": "Хочу заработать",
"next": 3
},
{
"text": "Ты точно нормальный?",
"next": 4
}
]
},
{
"id": 3,
"text": "(потирает руки) Оооо, братан, работа огонь! Цифры взламывать будем! (внезапно замолкает, прислушивается) Ты слышал? Нет? Ну ладно... (быстро) Но сначала... (достаёт из кармана пакетик) Хочешь попробовать? Для смелости...",
"answers": [
{
"text": "Давай попробую",
"next": 5
},
{
"text": "Нет, я не употребляю",
"next": 6
}
]
},
{
"id": 4,
"text": "(нервно смеётся) Нормальный? Да я самый нормальный тут! (вдруг серьёзнеет) Вот только вчера... Нет, не буду рассказывать... (глаза бегают) Ты работу хочешь или нет?",
"answers": [
{
"text": "Хочу работу",
"next": 3
},
{
"text": "Ты пугаешь меня",
"next": 7
}
]
},
{
"id": 5,
"text": "(радостно) Ооо, наш человек! (суёт пакетик) На вот, только не всё сразу, а то... (делает широкий жест руками) Бах! И тебя нет! Ха-ха! Ладно, заходи завтра, когда... ну... разберёшься со своими делами. Я тебе всё расскажу.",
"answers": [
{
"text": "Ладно, приду завтра",
"next": 8,
"quest_start": "hack_job"
},
{
"text": "Может лучше прямо сейчас?",
"next": 9
}
]
},
{
"id": 6,
"text": "(разочарованно) Фу, скукота... (пожимает плечами) Ну ладно, работа всё равно есть. Заходи завтра, я тебе всё расскажу. Только... (понижает голос) Никому не говори, ладно?",
"answers": [
{
"text": "Хорошо, приду завтра",
"next": 8,
"quest_start": "hack_job"
},
{
"text": "А можно подробнее?",
"next": 10
}
]
},
{
"id": 7,
"text": "(внезапно злится) Пугаю? Да я тебя... (резко успокаивается) Ладно, братан, иди отсюда. Не для тебя эта работа. (начинает что-то бормотать себе под нос)",
"answers": [ "answers": [
{ {
"text": "Договорились",
"end": true "end": true
} }
] ]
},
{
"id": 8,
"text": "(кивает) Молодец. Запомни: подвал за углом, охраннику скажешь... (шёпотом) 'берёзовый сок'. Он тебя пропустит. И... (внезапно хватает за руку) Только никому, понял? Ни-ко-му!",
"answers": [
{
"end": true
}
]
},
{
"id": 9,
"text": "(панически) Сейчас? Нет, нет, нет! (осматривается) Слишком много... глаз. Завтра. Только завтра. (начинает быстро уходить)",
"answers": [
{
"end": true
}
]
},
{
"id": 10,
"text": "(нервно оглядывается) Подробнее? Ну... (понижает голос) Есть подвал. Там компы. Надо... ну... (делает движение пальцами как при печати) Взламывать. Охраннику скажешь 'берёзовый сок' - пропустит. Всё. Больше ничего не знаю. (начинает чесаться)",
"answers": [
{
"text": "Понятно, приду завтра",
"next": 8,
"quest_start": "hack_job"
},
{
"text": "Это незаконно!",
"next": 11
}
]
},
{
"id": 11,
"text": "(истерично смеётся) Законно? Ха! В этом городе? (внезапно серьёзнеет) Ладно, иди отсюда, мальчик. Ищи себе 'законную' работу. (поворачивается спиной)",
"answers": [
{
"end": true
}
]
},
{
"id": "hack_job",
"type": "quest",
"title": "Взлом в подвале",
"description": "Нужно проникнуть в подвал, сказав охраннику пароль 'берёзовый сок', и взломать данные",
"location": "/locations/basement.json",
"reward": "500 кредитов",
"next": 8
} }
] ]
} }

View File

@@ -2,29 +2,43 @@
"name": "Охранник", "name": "Охранник",
"avatar": "/images/npc/guard.jpg", "avatar": "/images/npc/guard.jpg",
"filename": "Oxranik.json", "filename": "Oxranik.json",
"npc_id": 5,
"dialogue_key": "oxranik/report",
"dialog": [ "dialog": [
{ {
"id": 0, "id": 0,
"text": "Стоять! Кто такой?", "text": "Стоять! Что нужно?",
"answers": [ "answers": [
{ {
"text": "Березовый сок", "text": "Я выполнил задания",
"next": 1, "next": 1
"required_quest": "hack_job"
}, },
{ {
"text": "Я ошибся дверью", "text": "Ничего, ошибся",
"end": true "end": true
} }
] ]
}, },
{ {
"id": 1, "id": 1,
"text": "(кивает) Проходи. Но предупреждаю - если что-то пойдет не так, я тебя не знал.", "text": "Так... Вижу, ты поработал. Неплохо для новичка. Держи награду и приходи за новыми заданиями завтра.",
"answers": [ "answers": [
{ {
"text": "Понял", "text": "Спасибо",
"quest_progress": "hack_job", "end": true
},
{
"text": "Что дальше?",
"next": 2
}
]
},
{
"id": 2,
"text": "Отдохни сегодня. Завтра будут новые поручения. Спроси у бармена - он знает.",
"answers": [
{
"text": "Понял, до завтра",
"end": true "end": true
} }
] ]

View File

@@ -1,42 +1,26 @@
{ {
"name": "Серега Пират", "name": "Бармен",
"avatar": "/images/npc/bartender.jpg", "avatar": "/images/npc/bartender.jpg",
"filename": "bartender.json", "filename": "bartender.json",
"npc_id": 2,
"dialogue_key": "bartender/dialogue",
"dialog": [ "dialog": [
{ {
"id": 0, "id": 0,
"text": "Ну что, дружок, застрял как муха в паутине? Или просто решил проверить, насколько крепки эти стены?", "text": "Привет, новичок. Вижу, ты уже поговорил со стражником. Ну что, готов к настоящей работе?",
"answers": [ "answers": [
{ {
"text": "Я... кажется, ошибся дверью.", "text": "Расскажи, что есть",
"end": true "next": 1
},
{
"text": "Мне сказали, тут можно «устроиться». От Галины.",
"next": 2
} }
] ]
}, },
{ {
"id": 2, "id": 1,
"text": "Ага, Галка-весточка. Слушай сюда: правила простые — садишься за комп, выполняешь два задания. Первое — по звукам угадаешь пароль. Второе — в финансовых документах найдёшь косяк. Справишься — получишь свои кровные. Не справишься... ну, сам понимаешь.", "text": "У меня есть пара контактов. Но сначала послушай того искателя приключений в углу - у него есть полезная информация для новичков.",
"answers": [ "answers": [
{ {
"text": "И где этот ваш комп?", "text": "Хорошо, поговорю с ним",
"next": 3
},
{
"text": "А сложно будет?",
"next": 3
}
]
},
{
"id": 3,
"text": "*кивает на потрёпанный системник в углу* Там всё включено. Разберёшься. Главное — уши навостри и глаза пошире открой. Как будешь готов — жми любую кнопку.",
"answers": [
{
"text": "*Подойти к компьютеру*",
"end": true "end": true
} }
] ]

View File

@@ -1,102 +1,26 @@
{ {
"name": "Саша Белый", "name": "Стражник",
"avatar": "/images/npc/guard.jpg", "avatar": "/images/npc/guard.jpg",
"filename": "guard.json", "filename": "guard.json",
"npc_id": 1,
"dialogue_key": "guard/intro",
"dialog": [ "dialog": [
{ {
"id": 0, "id": 0,
"text": "А вот и новенький подъехал… Чё, глаза такие круглые? Добро пожаловать в Realternity Moscow City, братан. Тут не экскурсия, так что уши на макушке держи.", "text": "Стой! Новенький? Добро пожаловать в наш город. Первое правило - хочешь выжить, ищи работу.",
"answers": [ "answers": [
{ {
"text": "Понял...", "text": "Где можно найти работу?",
"next": 1
},
{
"text": "Что это за место?",
"next": 1 "next": 1
} }
] ]
}, },
{ {
"id": 1, "id": 1,
"text": "Город у нас большой, светится неоном, как новогодняя ёлка, но под этой мишурой — помойка, крысы да волки. Вверх глянешь — небоскрёбы корпораций, вниз — подземка, где людей за карточку еды режут. Каждый тут сам за себя.", "text": "Начни с таверны. Бармен всегда в курсе, кому нужны руки. Иди к нему.",
"answers": [ "answers": [
{ {
"text": "И что делать?", "text": "Спасибо, пойду в таверну",
"next": 2
},
{
"text": "Звучит жутко...",
"next": 2
}
]
},
{
"id": 2,
"text": "Первое правило — хочешь жить, ищи работу. Деньги — это воздух. Без них ты тут не человек, а мусор под ногами.",
"answers": [
{
"text": "А если работы нет?",
"next": 3
},
{
"text": "Где искать?",
"next": 4
}
]
},
{
"id": 3,
"text": "Нет работы? Ну, значит, ищешь... альтернативы. Тут таких путей — как тараканов в общаге. Главное — не ной, действуй.",
"answers": [
{
"text": "Какие альтернативы?",
"next": 5
},
{
"text": "Понятно...",
"next": 6
}
]
},
{
"id": 4,
"text": "Короче, если совсем не врубаешься, топай в Центр Трудоустройства «Нижний Эшелон». Там помогут... ну, если не кинут. Смотри в оба, братан.",
"answers": [
{
"text": "Спасибо за совет",
"next": 6
},
{
"text": "А где это?",
"next": 6
}
]
},
{
"id": 5,
"text": "Честный ты, нечестный — неважно. Главное — чтоб ты выбрал, по какой дорожке топать. По свету, по тени… или, может, будешь тем, кто идёт посередине и стреляет в обе стороны. Тут за всё платят — вопрос только, чем.",
"answers": [
{
"text": "Ясно...",
"next": 6
},
{
"text": "Страшновато",
"next": 6
}
]
},
{
"id": 6,
"text": "И запомни: здесь не детский сад. Никто за ручку водить не будет. Или ты адаптируешься… или сгниёшь на подворотне. Всё просто.",
"answers": [
{
"text": "Понял, спасибо",
"end": true
},
{
"text": "До встречи",
"end": true "end": true
} }
] ]

Binary file not shown.

Binary file not shown.

Binary file not shown.

0
rltn.online Normal file
View File

View File

@@ -1 +1 @@
{"time":"2025-04-20T19:17:09.736Z","lastReal":1756219619275} {"time":"2025-12-05T08:30:14.152Z","lastReal":1758687967327}

1077
server.js

File diff suppressed because it is too large Load Diff

258
server/README.md Normal file
View File

@@ -0,0 +1,258 @@
# Серверная часть EEV_Proj
## Описание
Серверная часть проекта EEV_Proj построена на Node.js и Express. Сервер обеспечивает API для клиентской части, управляет WebSocket соединениями и взаимодействует с базой данных PostgreSQL.
## Структура
```
server/
├── server.js # Основной файл сервера
├── db.js # Подключение к базе данных
├── db1.js # Альтернативная конфигурация БД
├── organizations.js # Логика организаций
├── ecosystem.config.js # Конфигурация PM2
└── README.md # Эта документация
```
## Основные компоненты
### server.js
Главный файл сервера, который:
- Инициализирует Express приложение
- Настраивает middleware
- Определяет API маршруты
- Запускает WebSocket сервер
- Обрабатывает игровую логику
### db.js
Модуль для работы с базой данных PostgreSQL:
- Подключение к БД
- Выполнение SQL запросов
- Управление транзакциями
### organizations.js
Логика для работы с организациями в игре:
- Создание организаций
- Управление меню организаций
- Обработка покупок
## API эндпоинты
### Аутентификация
- `POST /api/auth/login` - Вход в систему
- `POST /api/auth/register` - Регистрация
### Интерьеры
- `GET /api/interiors/:id/definition` - Получение определения интерьера
- `POST /api/interiors/:id/enter` - Вход в интерьер
### Объекты города
- `GET /api/city_objects/:id/interior` - Получение информации об объекте
### Экономика
- `GET /api/economy/balance` - Получение баланса
- `POST /api/economy/purchase` - Покупка предметов
- `GET /api/economy/inventory` - Получение инвентаря
### Квесты
- `GET /api/quests/progress` - Прогресс квестов
- `POST /api/quests/complete` - Завершение квеста
### Пользователи
- `GET /api/users/status` - Статус пользователей
- `GET /api/users/:id/profile` - Профиль пользователя
### Сообщения
- `GET /api/messages/:contactId` - Получение сообщений
- `POST /api/messages/send` - Отправка сообщения
- `POST /api/messages-read/:contactId` - Отметка сообщений как прочитанных
## WebSocket события
### Клиент → Сервер
- `playerMovement` - Движение игрока
- `interiorChange` - Смена интерьера
- `economy:getBalance` - Запрос баланса
- `economy:getInventory` - Запрос инвентаря
- `economy:updateStats` - Обновление статистики
- `economy:removeItem` - Удаление предмета
- `voiceChatToggle` - Переключение голосового чата
### Сервер → Клиент
- `economy:balanceChanged` - Изменение баланса
- `economy:inventory` - Обновление инвентаря
- `gameTime:update` - Обновление игрового времени
- `us` - Обновление статуса пользователей
## Установка и запуск
### Зависимости
```bash
npm install
```
### Переменные окружения
Создайте файл `.env` в корне проекта:
```dotenv
# Сервер
PORT=4000
NODE_ENV=development
# База данных
DB_HOST=localhost
DB_PORT=5432
DB_USER=your_username
DB_PASS=your_password
DB_NAME=your_database
# JWT
JWT_SECRET=your_jwt_secret
```
### Запуск
```bash
# Обычный запуск
node server.js
# Через PM2
pm2 start ecosystem.config.js
# В режиме разработки
npm run dev
```
## База данных
### Основные таблицы
#### users
- `id` - Уникальный идентификатор
- `username` - Имя пользователя
- `email` - Email адрес
- `password_hash` - Хеш пароля
- `created_at` - Дата создания
#### interiors
- `id` - Уникальный идентификатор
- `name` - Название интерьера
- `glb_path` - Путь к 3D модели
- `spawn_x`, `spawn_y`, `spawn_z` - Координаты входа
- `exit_x`, `exit_y`, `exit_z` - Координаты выхода
#### city_objects
- `id` - Уникальный идентификатор
- `name` - Название объекта
- `interior_id` - Ссылка на интерьер
- `position_x`, `position_y`, `position_z` - Позиция в мире
#### organizations
- `id` - Уникальный идентификатор
- `name` - Название организации
- `type` - Тип организации
- `menu` - JSON с меню организации
#### inventory
- `id` - Уникальный идентификатор
- `user_id` - Ссылка на пользователя
- `item_id` - Ссылка на предмет
- `quantity` - Количество
## Безопасность
### JWT токены
- Используются для аутентификации
- Хранятся в localStorage клиента
- Передаются в заголовке Authorization
### Валидация данных
- Проверка входных данных
- Санитизация SQL запросов
- Защита от SQL инъекций
### CORS
- Настроен для разрешения запросов с клиента
- Ограничен по доменам в продакшене
## Мониторинг и логирование
### PM2
- Управление процессами
- Автоперезапуск при сбоях
- Логирование
### Логи
- Логирование ошибок
- Логирование API запросов
- Логирование WebSocket событий
## Масштабирование
### Кластеризация
- Поддержка нескольких процессов через PM2
- Балансировка нагрузки
- Общие WebSocket соединения
### Кэширование
- Redis для кэширования данных
- Кэширование статических файлов
- Кэширование результатов запросов
## Развертывание
### Docker
```dockerfile
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 4000
CMD ["node", "server.js"]
```
### Nginx
Настройте Nginx для проксирования API запросов на порт 4000.
### SSL
Настройте HTTPS для безопасного соединения.
## Тестирование
### Unit тесты
```bash
npm test
```
### Интеграционные тесты
```bash
npm run test:integration
```
### Нагрузочное тестирование
```bash
npm run test:load
```
## Отладка
### Логи
- Проверьте логи PM2: `pm2 logs`
- Проверьте логи приложения в консоли
### База данных
- Проверьте подключение к БД
- Выполните тестовые запросы
### WebSocket
- Проверьте подключение клиента
- Проверьте события в консоли браузера
## Поддержка
При возникновении проблем:
1. Проверьте логи сервера
2. Проверьте подключение к базе данных
3. Проверьте конфигурацию
4. Создайте issue в репозитории

View File

@@ -169,7 +169,7 @@ module.exports = function(io, onlineUsers) {
let thirst = parseFloat(rows[0].thirst ?? 100); let thirst = parseFloat(rows[0].thirst ?? 100);
if (balance < price) { if (balance < price) {
const sock = onlineUsers[req.user.id]; const sock = onlineUsers.get(req.user.id);
if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вам недостаточно средств для покупки ${itemDef.name}` }); if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вам недостаточно средств для покупки ${itemDef.name}` });
return res.status(400).json({ error: 'insufficient funds' }); return res.status(400).json({ error: 'insufficient funds' });
} }
@@ -196,7 +196,7 @@ module.exports = function(io, onlineUsers) {
satiety = Math.min(100, satiety + parseFloat(itemDef.hunger_gain)); satiety = Math.min(100, satiety + parseFloat(itemDef.hunger_gain));
thirst = Math.min(100, thirst + parseFloat(itemDef.thirst_gain)); thirst = Math.min(100, thirst + parseFloat(itemDef.thirst_gain));
const sock = onlineUsers[req.user.id]; const sock = onlineUsers.get(req.user.id);
if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вы купили ${itemDef.name}` }); if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вы купили ${itemDef.name}` });
res.json({ success: true, balance, satiety, thirst }); res.json({ success: true, balance, satiety, thirst });

View File

@@ -10,6 +10,8 @@ import GameWrapper from './components/GameWrapper';
import RequireProfile from './components/RequireProfile'; import RequireProfile from './components/RequireProfile';
import MapEditor from './pages/MapEditor'; import MapEditor from './pages/MapEditor';
import InteriorEditor from './pages/InteriorEditor'; import InteriorEditor from './pages/InteriorEditor';
import CollisionEditor from './pages/CollisionEditor';
import EnhancedCollisionEditor from './pages/EnhancedCollisionEditor';
export default function App() { export default function App() {
const [isAuth, setIsAuth] = useState(!!localStorage.getItem('token')); const [isAuth, setIsAuth] = useState(!!localStorage.getItem('token'));
@@ -77,6 +79,28 @@ export default function App() {
} }
/> />
{/* редактор коллизий */}
<Route
path="/collision-editor"
element={
isAuth
? <RequireProfile>
<CollisionEditor />
</RequireProfile>
: <Navigate to="/login" replace/>
}
/>
<Route
path="/enhanced-collision-editor"
element={
isAuth
? <RequireProfile>
<EnhancedCollisionEditor />
</RequireProfile>
: <Navigate to="/login" replace/>
}
/>
{/* всё остальное */} {/* всё остальное */}
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>

File diff suppressed because it is too large Load Diff

611
src/api/README.md Normal file
View File

@@ -0,0 +1,611 @@
# API EEV_Proj
## Обзор
API функции для взаимодействия с серверной частью проекта EEV_Proj. Все функции используют современный JavaScript и обеспечивают единообразный интерфейс для работы с сервером.
## Структура
```
api/
├── auth.js # Функции аутентификации
└── README.md # Эта документация
```
## auth.js
### Функции аутентификации
#### getUsersStatus(token)
Получает статус всех пользователей в системе.
**Параметры:**
- `token` (string) - JWT токен аутентификации
**Возвращает:**
- Promise<Array> - Массив пользователей с их статусами
**Пример использования:**
```javascript
import { getUsersStatus } from './api/auth.js';
const token = localStorage.getItem('token');
try {
const users = await getUsersStatus(token);
console.log('Пользователи:', users);
} catch (error) {
console.error('Ошибка получения статуса пользователей:', error);
}
```
#### loadUserInfo(userId, token)
Загружает информацию о конкретном пользователе.
**Параметры:**
- `userId` (string|number) - ID пользователя
- `token` (string) - JWT токен аутентификации
**Возвращает:**
- Promise<Object> - Объект с информацией о пользователе
**Пример использования:**
```javascript
import { loadUserInfo } from './api/auth.js';
const token = localStorage.getItem('token');
try {
const userInfo = await loadUserInfo('123', token);
console.log('Информация о пользователе:', userInfo);
} catch (error) {
console.error('Ошибка загрузки информации о пользователе:', error);
}
```
## Создание новых API функций
### Шаблон API функции
```javascript
/**
* Описание функции
* @param {string} param1 - Описание параметра 1
* @param {number} param2 - Описание параметра 2
* @returns {Promise<Object>} Описание возвращаемого значения
*/
export async function apiFunction(param1, param2) {
try {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('Токен не найден');
}
const response = await fetch(`/api/endpoint`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ param1, param2 })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Ошибка API функции:', error);
throw error;
}
}
```
### Принципы
1. **Единообразие** - Все функции следуют одному паттерну
2. **Обработка ошибок** - Всегда обрабатывайте ошибки
3. **Валидация** - Проверяйте входные параметры
4. **Логирование** - Логируйте ошибки для отладки
5. **Типизация** - Используйте JSDoc для документирования типов
## Обработка ошибок
### Типы ошибок
```javascript
// Сетевые ошибки
class NetworkError extends Error {
constructor(message, status) {
super(message);
this.name = 'NetworkError';
this.status = status;
}
}
// Ошибки аутентификации
class AuthError extends Error {
constructor(message) {
super(message);
this.name = 'AuthError';
}
}
// Ошибки валидации
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
```
### Обработка в компонентах
```javascript
import { apiFunction } from './api/api.js';
const MyComponent = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleApiCall = async () => {
try {
setLoading(true);
setError(null);
const result = await apiFunction('param1', 'param2');
setData(result);
} catch (error) {
if (error.name === 'AuthError') {
// Перенаправление на страницу входа
navigate('/login');
} else if (error.name === 'ValidationError') {
// Показать ошибку валидации
setError(`Ошибка в поле ${error.field}: ${error.message}`);
} else {
// Общая ошибка
setError(error.message);
}
} finally {
setLoading(false);
}
};
return (
<div>
{loading && <div>Загрузка...</div>}
{error && <div className="error">{error}</div>}
{data && <div>{/* Отображение данных */}</div>}
<button onClick={handleApiCall}>Вызвать API</button>
</div>
);
};
```
## Кэширование
### Простое кэширование
```javascript
const cache = new Map();
export async function cachedApiCall(key, apiFunction) {
if (cache.has(key)) {
const { data, timestamp } = cache.get(key);
const now = Date.now();
// Кэш действителен 5 минут
if (now - timestamp < 5 * 60 * 1000) {
return data;
}
}
try {
const data = await apiFunction();
cache.set(key, { data, timestamp: Date.now() });
return data;
} catch (error) {
throw error;
}
}
```
### Использование
```javascript
import { cachedApiCall } from './api/cache.js';
import { getUsersStatus } from './api/auth.js';
const loadUsers = async () => {
const token = localStorage.getItem('token');
return await cachedApiCall('users-status', () => getUsersStatus(token));
};
```
## Retry логика
### Автоматические повторы
```javascript
/**
* Выполняет API вызов с автоматическими повторами
* @param {Function} apiFunction - Функция API
* @param {number} maxRetries - Максимальное количество повторов
* @param {number} delay - Задержка между повторами в мс
*/
export async function retryApiCall(apiFunction, maxRetries = 3, delay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await apiFunction();
} catch (error) {
lastError = error;
if (attempt === maxRetries) {
throw lastError;
}
// Ждем перед следующим попыткой
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
}
```
### Использование
```javascript
import { retryApiCall } from './api/retry.js';
import { getUsersStatus } from './api/auth.js';
const loadUsersWithRetry = async () => {
const token = localStorage.getItem('token');
return await retryApiCall(() => getUsersStatus(token), 3, 1000);
};
```
## Batch запросы
### Группировка запросов
```javascript
/**
* Выполняет несколько API запросов параллельно
* @param {Array<Function>} apiFunctions - Массив функций API
* @returns {Promise<Array>} Массив результатов
*/
export async function batchApiCalls(apiFunctions) {
try {
const results = await Promise.allSettled(apiFunctions.map(fn => fn()));
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
console.error(`Ошибка в запросе ${index}:`, result.reason);
return null;
}
});
} catch (error) {
console.error('Ошибка batch запросов:', error);
throw error;
}
}
```
### Использование
```javascript
import { batchApiCalls } from './api/batch.js';
import { getUsersStatus, loadUserInfo } from './api/auth.js';
const loadAllData = async () => {
const token = localStorage.getItem('token');
const results = await batchApiCalls([
() => getUsersStatus(token),
() => loadUserInfo('123', token),
() => loadUserInfo('456', token)
]);
const [users, user1, user2] = results;
return { users, user1, user2 };
};
```
## WebSocket API
### Подключение
```javascript
import { io } from 'socket.io-client';
class WebSocketAPI {
constructor() {
this.socket = null;
this.isConnected = false;
}
connect(token) {
const serverUrl = window.location.hostname === 'localhost'
? 'http://localhost:4000'
: window.location.origin;
this.socket = io(serverUrl, {
transports: ['websocket', 'polling'],
auth: { token },
timeout: 20000
});
this.socket.on('connect', () => {
this.isConnected = true;
console.log('WebSocket подключен');
});
this.socket.on('disconnect', () => {
this.isConnected = false;
console.log('WebSocket отключен');
});
return this.socket;
}
disconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
this.isConnected = false;
}
}
emit(event, data) {
if (this.socket && this.isConnected) {
this.socket.emit(event, data);
} else {
console.warn('WebSocket не подключен');
}
}
on(event, callback) {
if (this.socket) {
this.socket.on(event, callback);
}
}
off(event, callback) {
if (this.socket) {
this.socket.off(event, callback);
}
}
}
export const wsAPI = new WebSocketAPI();
```
### Использование
```javascript
import { wsAPI } from './api/websocket.js';
// Подключение
const token = localStorage.getItem('token');
wsAPI.connect(token);
// Отправка события
wsAPI.emit('playerMovement', { x: 100, y: 0, z: 200 });
// Подписка на события
wsAPI.on('economy:balanceChanged', ({ userId, newBalance }) => {
console.log('Баланс изменился:', newBalance);
});
// Отписка
wsAPI.off('economy:balanceChanged');
```
## Тестирование API
### Mock функции
```javascript
// __mocks__/api/auth.js
export const getUsersStatus = jest.fn();
export const loadUserInfo = jest.fn();
// Сброс моков
beforeEach(() => {
getUsersStatus.mockClear();
loadUserInfo.mockClear();
});
```
### Тесты
```javascript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { getUsersStatus } from './api/auth.js';
// Мокаем модуль
jest.mock('./api/auth.js');
describe('API Functions', () => {
it('загружает статус пользователей', async () => {
const mockUsers = [
{ id: 1, name: 'User 1', status: 'online' },
{ id: 2, name: 'User 2', status: 'offline' }
];
getUsersStatus.mockResolvedValue(mockUsers);
const result = await getUsersStatus('token');
expect(result).toEqual(mockUsers);
expect(getUsersStatus).toHaveBeenCalledWith('token');
});
it('обрабатывает ошибки API', async () => {
const errorMessage = 'Unauthorized';
getUsersStatus.mockRejectedValue(new Error(errorMessage));
await expect(getUsersStatus('invalid-token')).rejects.toThrow(errorMessage);
});
});
```
## Мониторинг и метрики
### Логирование API вызовов
```javascript
class APIMonitor {
constructor() {
this.calls = [];
this.errors = [];
}
logCall(endpoint, method, duration, success) {
const call = {
endpoint,
method,
duration,
success,
timestamp: Date.now()
};
this.calls.push(call);
// Ограничиваем размер массива
if (this.calls.length > 1000) {
this.calls.shift();
}
}
logError(endpoint, method, error) {
const errorLog = {
endpoint,
method,
error: error.message,
timestamp: Date.now()
};
this.errors.push(errorLog);
if (this.errors.length > 100) {
this.errors.shift();
}
}
getStats() {
const totalCalls = this.calls.length;
const successfulCalls = this.calls.filter(call => call.success).length;
const errorCalls = this.errors.length;
const avgDuration = this.calls.reduce((sum, call) => sum + call.duration, 0) / totalCalls;
return {
totalCalls,
successfulCalls,
errorCalls,
successRate: (successfulCalls / totalCalls) * 100,
avgDuration
};
}
}
export const apiMonitor = new APIMonitor();
```
### Использование в API функциях
```javascript
import { apiMonitor } from './api/monitor.js';
export async function monitoredApiCall(endpoint, options) {
const startTime = Date.now();
try {
const response = await fetch(endpoint, options);
const duration = Date.now() - startTime;
apiMonitor.logCall(endpoint, options.method || 'GET', duration, response.ok);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
const duration = Date.now() - startTime;
apiMonitor.logCall(endpoint, options.method || 'GET', duration, false);
apiMonitor.logError(endpoint, options.method || 'GET', error);
throw error;
}
}
```
## Безопасность
### Валидация токенов
```javascript
export function validateToken(token) {
if (!token) {
throw new Error('Токен не предоставлен');
}
// Проверяем формат JWT токена
const tokenParts = token.split('.');
if (tokenParts.length !== 3) {
throw new Error('Неверный формат токена');
}
try {
// Декодируем payload
const payload = JSON.parse(atob(tokenParts[1]));
// Проверяем срок действия
if (payload.exp && Date.now() >= payload.exp * 1000) {
throw new Error('Токен истек');
}
return payload;
} catch (error) {
throw new Error('Неверный токен');
}
}
```
### Санитизация данных
```javascript
export function sanitizeInput(input) {
if (typeof input !== 'string') {
return input;
}
// Удаляем потенциально опасные символы
return input
.replace(/[<>]/g, '')
.replace(/javascript:/gi, '')
.trim();
}
export function sanitizeObject(obj) {
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
sanitized[key] = sanitizeInput(value);
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = sanitizeObject(value);
} else {
sanitized[key] = value;
}
}
return sanitized;
}
```

View File

@@ -34,3 +34,43 @@ export async function registerStep1(data) {
return r.json(); return r.json();
} }
export const getUsersStatus = async (token) => {
try {
const response = await fetch('/api/users/status', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch users status');
}
return await response.json();
} catch (error) {
console.error('Error fetching users status:', error);
throw error;
}
};
export const loadUserInfo = async (userId, token) => {
try {
const response = await fetch(`/api/users/${userId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch user info');
}
return await response.json();
} catch (error) {
console.error('Error fetching user info:', error);
throw error;
}
};

View File

@@ -8,32 +8,29 @@ export const useDialogManager = () => {
const [currentForm, setCurrentForm] = useState(null); 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) => { const markDialogAsListened = async (npcId, dialogueKey) => {
try { 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>
const filename = jsonFilename.split('/').pop().split('\\').pop();
console.log('Normalized filename:', filename);
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD>111<31>");
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
const response = await fetch('/api/listen', { const response = await fetch('/api/quests/mark-dialog-listened', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
// player_id больше не обязателен: сервер возьмёт его из токена/сессии при наличии npc_id: npcId,
json_filename: filename dialogue_key: dialogueKey
}) })
}); });
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD><EFBFBD>3455654");
if (!response.ok) { if (!response.ok) {
const txt = await response.text().catch(() => ''); const txt = await response.text().catch(() => '');
console.error('Ошибка при записи прослушанного:', response.status, txt); console.error('Ошибка при записи прослушанного диалога:', response.status, txt);
} else {
console.log('Диалог успешно отмечен как прослушанный');
} }
} catch (error) { } catch (error) {
console.error('Ошибка сети при записи прослушанного:', error); console.error('Ошибка сети при записи прослушанного диалога:', error);
} }
}; };
@@ -44,21 +41,32 @@ export const useDialogManager = () => {
setCurrentDialog(data); setCurrentDialog(data);
setDialogIndex(0); setDialogIndex(0);
setShowDialog(true); setShowDialog(true);
// Получаем dialogue_key из JSON или используем npcId как fallback
const dialogueKey = data.dialogue_key || npcId;
// Записываем начало прослушивания диалога
await markDialogAsListened(npcId, dialogueKey);
} catch (error) { } catch (error) {
console.error('<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', error); console.error('Ошибка загрузки диалога:', error);
} }
}; };
const handleAnswerSelect = async (answer) => { 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] Answer object:', answer);
console.log('[Debug] "end" in answer:', 'end' in answer); // <- <20><><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> end?
if (answer.end !== undefined) { if (answer.end !== undefined) {
console.log('[Debug] Dialog end triggered!'); 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>
if (currentDialog?.filename) { // При завершении диалога записываем финальное взаимодействие
await markDialogAsListened(currentDialog.filename); if (currentDialog) {
console.log("<22><><EFBFBD><EFBFBD><EFBFBD> <20> <20><> <20><><EFBFBD><EFBFBD>"); const npcId = currentDialog.npc_id;
const dialogueKey = currentDialog.dialogue_key || currentDialog.filename?.replace('.json', '');
if (npcId && dialogueKey) {
await markDialogAsListened(npcId, dialogueKey);
} }
}
setShowDialog(false); setShowDialog(false);
} else if (answer.next !== undefined) { } else if (answer.next !== undefined) {
if (typeof answer.next === 'string' && answer.next.startsWith('form_')) { if (typeof answer.next === 'string' && answer.next.startsWith('form_')) {

340
src/components/README.md Normal file
View File

@@ -0,0 +1,340 @@
# Компоненты EEV_Proj
## Обзор
Компоненты React, используемые в проекте EEV_Proj. Все компоненты следуют принципам функционального программирования и используют хуки React.
## Структура
```
components/
├── DialogSystem/ # Система диалогов
│ ├── DialogManager.js # Хук для управления диалогами
│ └── DialogWindow.js # Окно диалога
├── GameWrapper.jsx # Обертка для игровой сцены
├── Inventory.jsx # Инвентарь игрока
├── Loading.jsx # Компонент загрузки
├── LoginScene.jsx # Сцена входа
├── OrgControlPanel.jsx # Панель управления организацией
├── RequireProfile.jsx # Компонент для профиля
└── README.md # Эта документация
```
## DialogSystem
### DialogManager.js
Хук для управления диалоговой системой.
**Использование:**
```javascript
const {
currentDialog,
dialogIndex,
showDialog,
loadDialog,
handleAnswerSelect,
setShowDialog
} = useDialogManager();
```
**Методы:**
- `loadDialog(npcId)` - Загружает диалог для NPC
- `handleAnswerSelect(answer)` - Обрабатывает выбор ответа
- `setShowDialog(show)` - Показывает/скрывает диалог
### DialogWindow.js
Компонент окна диалога.
**Props:**
- `dialog` - Объект диалога
- `dialogIndex` - Индекс текущего узла
- `onAnswerSelect` - Callback для выбора ответа
- `onClose` - Callback для закрытия
## GameWrapper.jsx
Обертка для игровой сцены, обеспечивающая правильное монтирование и размонтирование.
**Props:**
- `children` - Дочерние компоненты
**Особенности:**
- Автоматическое управление жизненным циклом
- Обработка ошибок
- Логирование
## Inventory.jsx
Компонент инвентаря игрока.
**Props:**
- `inventory` - Массив предметов
- `onClose` - Callback для закрытия
- `onItemAction` - Callback для действий с предметами
**Функциональность:**
- Отображение списка предметов
- Действия с предметами (использовать, выкинуть)
- Фильтрация и сортировка
## Loading.jsx
Компонент загрузки с анимацией.
**Props:**
- `message` - Сообщение загрузки
- `progress` - Прогресс (0-100)
**Особенности:**
- Анимированный спиннер
- Прогресс-бар
- Кастомные сообщения
## LoginScene.jsx
Сцена входа в игру.
**Функциональность:**
- Форма входа
- Валидация данных
- Обработка ошибок
- Перенаправление после входа
## OrgControlPanel.jsx
Панель управления организацией.
**Props:**
- `org` - Объект организации
- `onClose` - Callback для закрытия
- `onBuyItem` - Callback для покупки
**Функциональность:**
- Отображение меню организации
- Покупка предметов
- Управление настройками
## RequireProfile.jsx
Компонент для проверки профиля пользователя.
**Props:**
- `children` - Дочерние компоненты
- `redirectTo` - Путь для перенаправления
**Функциональность:**
- Проверка авторизации
- Перенаправление неавторизованных пользователей
- Защита маршрутов
## Создание новых компонентов
### Шаблон компонента
```javascript
import React from 'react';
/**
* Описание компонента
* @param {Object} props - Свойства компонента
* @param {string} props.title - Заголовок
* @param {Function} props.onClick - Callback для клика
*/
const NewComponent = ({ title, onClick }) => {
return (
<div className="new-component">
<h2>{title}</h2>
<button onClick={onClick}>
Нажми меня
</button>
</div>
);
};
export default NewComponent;
```
### Принципы
1. **Функциональные компоненты** - Используйте функциональные компоненты с хуками
2. **Props валидация** - Добавляйте PropTypes или TypeScript
3. **JSDoc** - Документируйте публичные API
4. **Единая ответственность** - Каждый компонент должен делать одну вещь
5. **Переиспользование** - Создавайте компоненты для переиспользования
### Стили
- Используйте CSS-in-JS или CSS модули
- Следуйте дизайн-системе проекта
- Обеспечивайте адаптивность
- Поддерживайте темную/светлую тему
## Тестирование
### Unit тесты
```javascript
import { render, screen, fireEvent } from '@testing-library/react';
import NewComponent from './NewComponent';
describe('NewComponent', () => {
it('отображает заголовок', () => {
render(<NewComponent title="Тест" />);
expect(screen.getByText('Тест')).toBeInTheDocument();
});
it('вызывает onClick при клике', () => {
const mockOnClick = jest.fn();
render(<NewComponent title="Тест" onClick={mockOnClick} />);
fireEvent.click(screen.getByRole('button'));
expect(mockOnClick).toHaveBeenCalled();
});
});
```
### Интеграционные тесты
```javascript
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import App from '../App';
const renderWithRouter = (component) => {
return render(
<BrowserRouter>
{component}
</BrowserRouter>
);
};
describe('App Integration', () => {
it('отображает главную страницу', () => {
renderWithRouter(<App />);
expect(screen.getByText('EEV_Proj')).toBeInTheDocument();
});
});
```
## Производительность
### Оптимизация
1. **React.memo** - Мемоизация компонентов
2. **useMemo** - Мемоизация вычислений
3. **useCallback** - Мемоизация функций
4. **Lazy loading** - Ленивая загрузка компонентов
### Пример оптимизации
```javascript
import React, { useMemo, useCallback } from 'react';
const OptimizedComponent = React.memo(({ items, onItemClick }) => {
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
const handleItemClick = useCallback((item) => {
onItemClick(item);
}, [onItemClick]);
return (
<ul>
{sortedItems.map(item => (
<li key={item.id} onClick={() => handleItemClick(item)}>
{item.name}
</li>
))}
</ul>
);
});
```
## Доступность
### ARIA атрибуты
```javascript
const AccessibleComponent = ({ label, value, onChange }) => {
return (
<div>
<label htmlFor="input">{label}</label>
<input
id="input"
type="text"
value={value}
onChange={onChange}
aria-describedby="help-text"
/>
<div id="help-text">Дополнительная информация</div>
</div>
);
};
```
### Клавиатурная навигация
```javascript
const KeyboardComponent = ({ onEnter, onEscape }) => {
const handleKeyDown = (event) => {
switch (event.key) {
case 'Enter':
onEnter();
break;
case 'Escape':
onEscape();
break;
}
};
return (
<div tabIndex={0} onKeyDown={handleKeyDown}>
Нажмите Enter или Escape
</div>
);
};
```
## Логирование
### Отладочная информация
```javascript
import { useEffect } from 'react';
const DebugComponent = ({ data }) => {
useEffect(() => {
console.log('Component mounted with data:', data);
return () => {
console.log('Component unmounting');
};
}, [data]);
return <div>Debug Component</div>;
};
```
### Обработка ошибок
```javascript
import { ErrorBoundary } from 'react-error-boundary';
const ErrorFallback = ({ error, resetErrorBoundary }) => {
return (
<div role="alert">
<p>Что-то пошло не так:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Попробовать снова</button>
</div>
);
};
const AppWithErrorBoundary = () => {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<App />
</ErrorBoundary>
);
};
```

View File

@@ -0,0 +1,279 @@
import * as THREE from 'three';
/**
* Менеджер камер
* Отвечает за создание, переключение и управление камерами
*/
export class CameraManager {
constructor() {
this.orthoCamera = null;
this.fpCamera = null;
this.currentCamera = null;
this.fpPitch = 0;
this.baseOffset = new THREE.Vector3(-200, 150, -200);
this.planarDist = Math.hypot(this.baseOffset.x, this.baseOffset.z);
this.radius = Math.hypot(this.planarDist, this.baseOffset.y);
this.baseAzimuth = Math.atan2(this.baseOffset.z, this.baseOffset.x);
this.basePolar = Math.atan2(this.baseOffset.y, this.planarDist);
this.cameraPitchOffset = 0;
this.maxPitch = THREE.MathUtils.degToRad(10);
this.zoom = 10;
this.minZoom = this.zoom * 0.1;
this.maxZoom = this.zoom * 3.5;
this.init();
}
/**
* Инициализация камер
*/
init() {
this.createOrthoCamera();
this.createFirstPersonCamera();
this.currentCamera = this.orthoCamera;
}
/**
* Создание ортографической камеры (вид сверху)
*/
createOrthoCamera() {
const aspect = window.innerWidth / window.innerHeight;
const frustumSize = 50;
this.orthoCamera = new THREE.OrthographicCamera(
frustumSize * aspect / -2,
frustumSize * aspect / 2,
frustumSize / 2,
frustumSize / -2,
1,
1000
);
this.orthoCamera.position.copy(this.baseOffset);
this.orthoCamera.lookAt(0, 0, 0);
this.orthoCamera.updateProjectionMatrix();
}
/**
* Создание камеры от первого лица
*/
createFirstPersonCamera() {
this.fpCamera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.fpCamera.position.set(0, 1.6, 0);
this.fpCamera.updateProjectionMatrix();
}
/**
* Переключение на камеру от первого лица
*/
switchToFirstPersonCamera(playerPosition, playerRotation) {
if (!this.fpCamera || !playerPosition) return;
this.currentCamera = this.fpCamera;
// Устанавливаем позицию камеры на уровне глаз игрока
const headHeight = 1.6;
this.fpCamera.position.set(
playerPosition.x,
playerPosition.y + headHeight,
playerPosition.z
);
// Небольшой сдвиг камеры вперед
const forward = new THREE.Vector3(0, 0, -0.08).applyEuler(
new THREE.Euler(0, playerRotation.y, 0)
);
this.fpCamera.position.add(forward);
// Направляем камеру в том же направлении, что и игрок
const direction = new THREE.Vector3(0, 0, -1);
direction.applyEuler(new THREE.Euler(0, playerRotation.y, 0));
this.fpCamera.lookAt(
this.fpCamera.position.clone().add(direction)
);
this.fpPitch = 0;
this.fpCamera.updateProjectionMatrix();
// Запрашиваем pointer lock для управления мышью
this.requestPointerLock();
}
/**
* Переключение на ортографическую камеру
*/
switchToOrthoCamera() {
this.currentCamera = this.orthoCamera;
this.fpPitch = 0;
// Выходим из pointer lock
this.exitPointerLock();
}
/**
* Запрос pointer lock для управления мышью
*/
requestPointerLock() {
if (document.body.requestPointerLock) {
document.body.requestPointerLock();
}
}
/**
* Выход из pointer lock
*/
exitPointerLock() {
if (document.exitPointerLock) {
document.exitPointerLock();
}
}
/**
* Обновление позиции ортографической камеры
*/
updateOrthoCameraPosition(playerPosition) {
if (!this.orthoCamera || !playerPosition) return;
const offset = this.baseOffset.clone();
offset.x += playerPosition.x;
offset.z += playerPosition.z;
this.orthoCamera.position.copy(offset);
this.orthoCamera.lookAt(playerPosition.x, 0, playerPosition.z);
this.orthoCamera.updateProjectionMatrix();
}
/**
* Обновление позиции камеры от первого лица
*/
updateFirstPersonCameraPosition(playerPosition, playerRotation) {
if (!this.fpCamera || !playerPosition) return;
const headHeight = 1.6;
this.fpCamera.position.set(
playerPosition.x,
playerPosition.y + headHeight,
playerPosition.z
);
// Сдвиг вперед
const forward = new THREE.Vector3(0, 0, -0.08).applyEuler(
new THREE.Euler(0, playerRotation.y, 0)
);
this.fpCamera.position.add(forward);
// Обновляем направление взгляда
const direction = new THREE.Vector3(0, 0, -1);
direction.applyEuler(new THREE.Euler(0, playerRotation.y, 0));
this.fpCamera.lookAt(
this.fpCamera.position.clone().add(direction)
);
this.fpCamera.updateProjectionMatrix();
}
/**
* Обработка движения мыши для камеры от первого лица
*/
handleMouseMove(deltaX, deltaY, sensitivity = 0.002) {
if (this.currentCamera !== this.fpCamera) return;
// Горизонтальный поворот
this.fpCamera.rotation.y -= deltaX * sensitivity;
// Вертикальный поворот с ограничениями
this.fpPitch -= deltaY * sensitivity;
this.fpPitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.fpPitch));
this.fpCamera.rotation.x = this.fpPitch;
this.fpCamera.updateProjectionMatrix();
}
/**
* Обработка колеса мыши для зума
*/
handleWheel(delta, sensitivity = 0.1) {
if (this.currentCamera !== this.orthoCamera) return;
this.zoom += delta * sensitivity;
this.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.zoom));
// Обновляем размеры frustum
const aspect = window.innerWidth / window.innerHeight;
const frustumSize = 50 / this.zoom;
this.orthoCamera.left = frustumSize * aspect / -2;
this.orthoCamera.right = frustumSize * aspect / 2;
this.orthoCamera.top = frustumSize / 2;
this.orthoCamera.bottom = frustumSize / -2;
this.orthoCamera.updateProjectionMatrix();
}
/**
* Обработка изменения размера окна
*/
handleResize() {
const aspect = window.innerWidth / window.innerHeight;
// Обновляем перспективную камеру
if (this.fpCamera) {
this.fpCamera.aspect = aspect;
this.fpCamera.updateProjectionMatrix();
}
// Обновляем ортографическую камеру
if (this.orthoCamera) {
const frustumSize = 50 / this.zoom;
this.orthoCamera.left = frustumSize * aspect / -2;
this.orthoCamera.right = frustumSize * aspect / 2;
this.orthoCamera.top = frustumSize / 2;
this.orthoCamera.bottom = frustumSize / -2;
this.orthoCamera.updateProjectionMatrix();
}
}
/**
* Получение текущей активной камеры
*/
getCurrentCamera() {
return this.currentCamera;
}
/**
* Получение ортографической камеры
*/
getOrthoCamera() {
return this.orthoCamera;
}
/**
* Получение камеры от первого лица
*/
getFirstPersonCamera() {
return this.fpCamera;
}
/**
* Проверка, активна ли камера от первого лица
*/
isFirstPersonActive() {
return this.currentCamera === this.fpCamera;
}
/**
* Очистка ресурсов
*/
dispose() {
// Камеры автоматически очищаются при удалении сцены
this.orthoCamera = null;
this.fpCamera = null;
this.currentCamera = null;
}
}

View File

@@ -0,0 +1,300 @@
import * as THREE from 'three';
/**
* Менеджер коллизий
* Отвечает за проверку столкновений игрока с объектами интерьера
*/
export class CollisionManager {
constructor(sceneManager) {
this.sceneManager = sceneManager;
this.playerRadius = 0.35; // Радиус игрока
this.playerHeight = 1.6; // Высота игрока
this.collisionCache = new Map(); // Кэш для оптимизации
this.cacheTimeout = 100; // Время жизни кэша в мс
this.lastCacheUpdate = 0;
this.init();
}
/**
* Инициализация менеджера коллизий
*/
init() {
console.log('CollisionManager инициализирован');
}
/**
* Проверка коллизий при движении игрока в интерьере
* @param {THREE.Vector3} currentPosition - Текущая позиция игрока
* @param {THREE.Vector3} targetPosition - Целевая позиция игрока
* @param {THREE.Vector3} direction - Направление движения
* @param {number} deltaTime - Время между кадрами
* @returns {THREE.Vector3} - Безопасная позиция игрока
*/
checkInteriorCollisions(currentPosition, targetPosition, direction, deltaTime) {
if (!this.sceneManager.interiorColliders || this.sceneManager.interiorColliders.length === 0) {
return targetPosition;
}
// Обновляем кэш коллизий если нужно
this.updateCollisionCache();
// Получаем коллайдеры из кэша
const colliders = this.collisionCache.get('interiorColliders') || [];
if (colliders.length === 0) {
return targetPosition;
}
// Проверяем коллизии по осям отдельно для более плавного движения
let safePosition = currentPosition.clone();
// Проверяем движение по X
if (Math.abs(direction.x) > 0.001) {
const xTestPosition = safePosition.clone();
xTestPosition.x = targetPosition.x;
if (!this.checkPlayerCollision(xTestPosition, colliders)) {
safePosition.x = targetPosition.x;
}
}
// Проверяем движение по Z
if (Math.abs(direction.z) > 0.001) {
const zTestPosition = safePosition.clone();
zTestPosition.z = targetPosition.z;
if (!this.checkPlayerCollision(zTestPosition, colliders)) {
safePosition.z = targetPosition.z;
}
}
return safePosition;
}
/**
* Проверка столкновения игрока с коллайдерами
* @param {THREE.Vector3} position - Позиция игрока
* @param {Array} colliders - Массив коллайдеров
* @returns {boolean} - true если есть столкновение
*/
checkPlayerCollision(position, colliders) {
// Создаем AABB для игрока
const playerBox = new THREE.Box3();
const playerMin = new THREE.Vector3(
position.x - this.playerRadius,
position.y,
position.z - this.playerRadius
);
const playerMax = new THREE.Vector3(
position.x + this.playerRadius,
position.y + this.playerHeight,
position.z + this.playerRadius
);
playerBox.setFromPoints([playerMin, playerMax]);
// Проверяем столкновения с каждым коллайдером
for (const collider of colliders) {
if (!collider.geometry || !collider.visible) continue;
// Пропускаем интерактивные объекты
if (collider.userData && (collider.userData.interactable || collider.userData.payload)) {
continue;
}
// Пропускаем сферы (хит-зоны)
if (collider.geometry.type === 'SphereGeometry') {
continue;
}
try {
// Обновляем мировую матрицу коллайдера
collider.updateMatrixWorld(true);
// Создаем AABB для коллайдера
const colliderBox = new THREE.Box3();
colliderBox.setFromObject(collider);
// Проверяем пересечение
if (playerBox.intersectsBox(colliderBox)) {
return true;
}
} catch (error) {
console.warn('Ошибка при проверке коллизии:', error);
continue;
}
}
return false;
}
/**
* Обновление кэша коллизий
*/
updateCollisionCache() {
const now = Date.now();
// Обновляем кэш только если прошло достаточно времени
if (now - this.lastCacheUpdate < this.cacheTimeout) {
return;
}
this.lastCacheUpdate = now;
// Очищаем старый кэш
this.collisionCache.clear();
// Собираем коллайдеры интерьера
const interiorColliders = this.sceneManager.interiorColliders || [];
const validColliders = [];
// Фильтруем и валидируем коллайдеры
for (const collider of interiorColliders) {
if (this.isValidCollider(collider)) {
validColliders.push(collider);
}
}
// Если коллайдеров мало, попробуем собрать их из группы интерьера
if (validColliders.length === 0 && this.sceneManager.interiorGroup) {
this.collectCollidersFromInteriorGroup(validColliders);
}
this.collisionCache.set('interiorColliders', validColliders);
}
/**
* Проверка валидности коллайдера
* @param {THREE.Object3D} collider - Объект для проверки
* @returns {boolean} - true если коллайдер валиден
*/
isValidCollider(collider) {
if (!collider || !collider.isMesh) return false;
if (!collider.geometry) return false;
if (!collider.visible) return false;
// Пропускаем интерактивные объекты
if (collider.userData && (collider.userData.interactable || collider.userData.payload)) {
return false;
}
// Пропускаем сферы (хит-зоны)
if (collider.geometry.type === 'SphereGeometry') {
return false;
}
return true;
}
/**
* Сбор коллайдеров из группы интерьера
* @param {Array} colliders - Массив для добавления коллайдеров
*/
collectCollidersFromInteriorGroup(colliders) {
if (!this.sceneManager.interiorGroup) return;
try {
this.sceneManager.interiorGroup.updateMatrixWorld(true);
this.sceneManager.interiorGroup.traverse((child) => {
if (this.isValidCollider(child)) {
colliders.push(child);
}
});
} catch (error) {
console.warn('Ошибка при сборе коллайдеров из группы интерьера:', error);
}
}
/**
* Проверка коллизий с интерактивными объектами
* @param {THREE.Vector3} position - Позиция игрока
* @returns {Object|null} - Данные интерактивного объекта или null
*/
checkInteriorInteractions(position) {
const interactables = this.sceneManager.interiorInteractables || [];
for (const interactable of interactables) {
if (!interactable.geometry || !interactable.visible) continue;
try {
interactable.updateMatrixWorld(true);
// Создаем сферу вокруг интерактивного объекта
const interactableBox = new THREE.Box3();
interactableBox.setFromObject(interactable);
// Создаем AABB для игрока
const playerBox = new THREE.Box3();
const playerMin = new THREE.Vector3(
position.x - this.playerRadius,
position.y,
position.z - this.playerRadius
);
const playerMax = new THREE.Vector3(
position.x + this.playerRadius,
position.y + this.playerHeight,
position.z + this.playerRadius
);
playerBox.setFromPoints([playerMin, playerMax]);
if (playerBox.intersectsBox(interactableBox)) {
return interactable.userData.payload || { type: 'interactable' };
}
} catch (error) {
console.warn('Ошибка при проверке взаимодействия:', error);
continue;
}
}
return null;
}
/**
* Получение безопасной позиции для телепортации
* @param {THREE.Vector3} targetPosition - Целевая позиция
* @returns {THREE.Vector3} - Безопасная позиция
*/
getSafeTeleportPosition(targetPosition) {
const colliders = this.collisionCache.get('interiorColliders') || [];
if (colliders.length === 0) {
return targetPosition.clone();
}
// Проверяем целевую позицию
if (!this.checkPlayerCollision(targetPosition, colliders)) {
return targetPosition.clone();
}
// Ищем ближайшую безопасную позицию
const searchRadius = 2.0;
const searchSteps = 8;
for (let radius = 0.5; radius <= searchRadius; radius += 0.5) {
for (let step = 0; step < searchSteps; step++) {
const angle = (step / searchSteps) * Math.PI * 2;
const testPosition = new THREE.Vector3(
targetPosition.x + Math.cos(angle) * radius,
targetPosition.y,
targetPosition.z + Math.sin(angle) * radius
);
if (!this.checkPlayerCollision(testPosition, colliders)) {
return testPosition;
}
}
}
// Если не нашли безопасную позицию, возвращаем исходную
return targetPosition.clone();
}
/**
* Очистка ресурсов
*/
dispose() {
this.collisionCache.clear();
console.log('CollisionManager очищен');
}
}

451
src/modules/GameCore.js Normal file
View File

@@ -0,0 +1,451 @@
import * as THREE from 'three';
import { SceneManager } from './SceneManager.js';
import { CameraManager } from './CameraManager.js';
import { PlayerManager } from './PlayerManager.js';
import { RendererManager } from './RendererManager.js';
import { InteriorManager } from './InteriorManager.js';
import { CollisionManager } from './CollisionManager.js';
/**
* Основной класс игры
* Координирует все модули и управляет игровым циклом
*/
export class GameCore {
constructor(container) {
this.container = container;
this.isRunning = false;
this.clock = new THREE.Clock();
// Инициализация модулей
this.sceneManager = new SceneManager();
this.cameraManager = new CameraManager();
this.collisionManager = new CollisionManager(this.sceneManager);
this.playerManager = new PlayerManager(this.sceneManager, this.collisionManager);
this.rendererManager = new RendererManager(container);
this.interiorManager = new InteriorManager(this.sceneManager);
// Состояние игры
this.moveInput = {
forward: false,
backward: false,
left: false,
right: false,
strafeLeft: false,
strafeRight: false
};
this.isInInterior = false;
this.currentExit = null;
this.init();
}
/**
* Инициализация игры
*/
async init() {
try {
// Ждем инициализации всех модулей
await this.playerManager.init();
// Настраиваем обработчики событий
this.setupEventListeners();
// Запускаем игровой цикл
this.start();
console.log('GameCore инициализирован успешно');
} catch (error) {
console.error('Ошибка инициализации GameCore:', error);
}
}
/**
* Настройка обработчиков событий
*/
setupEventListeners() {
// Обработка клавиатуры
document.addEventListener('keydown', this.handleKeyDown.bind(this));
document.addEventListener('keyup', this.handleKeyUp.bind(this));
// Обработка мыши
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
document.addEventListener('wheel', this.handleWheel.bind(this));
// Обработка изменения размера окна
window.addEventListener('resize', this.handleResize.bind(this));
// Обработка кликов по сцене
this.setupClickHandlers();
}
/**
* Настройка обработчиков кликов
*/
setupClickHandlers() {
const canvas = this.rendererManager.getDomElement();
if (!canvas) return;
canvas.addEventListener('click', this.handleSceneClick.bind(this));
canvas.addEventListener('pointerdown', this.handleSceneClick.bind(this));
}
/**
* Обработка нажатия клавиш
*/
handleKeyDown(event) {
switch (event.code) {
case 'KeyW':
case 'ArrowUp':
this.moveInput.forward = true;
break;
case 'KeyS':
case 'ArrowDown':
this.moveInput.backward = true;
break;
case 'KeyA':
case 'ArrowLeft':
this.moveInput.left = true;
break;
case 'KeyD':
case 'ArrowRight':
this.moveInput.right = true;
break;
case 'KeyQ':
this.moveInput.strafeLeft = true;
break;
case 'KeyE':
this.moveInput.strafeRight = true;
break;
}
}
/**
* Обработка отпускания клавиш
*/
handleKeyUp(event) {
switch (event.code) {
case 'KeyW':
case 'ArrowUp':
this.moveInput.forward = false;
break;
case 'KeyS':
case 'ArrowDown':
this.moveInput.backward = false;
break;
case 'KeyA':
case 'ArrowLeft':
this.moveInput.left = false;
break;
case 'KeyD':
case 'ArrowRight':
this.moveInput.right = false;
break;
case 'KeyQ':
this.moveInput.strafeLeft = false;
break;
case 'KeyE':
this.moveInput.strafeRight = false;
break;
}
}
/**
* Обработка движения мыши
*/
handleMouseMove(event) {
if (this.cameraManager.isFirstPersonActive()) {
this.cameraManager.handleMouseMove(event.movementX, event.movementY);
}
}
/**
* Обработка колеса мыши
*/
handleWheel(event) {
this.cameraManager.handleWheel(event.deltaY);
}
/**
* Обработка изменения размера окна
*/
handleResize() {
this.rendererManager.handleResize();
this.cameraManager.handleResize();
}
/**
* Обработка кликов по сцене
*/
handleSceneClick(event) {
if (!this.rendererManager.isReady() || !this.cameraManager.getCurrentCamera()) return;
const rect = this.rendererManager.getDomElement().getBoundingClientRect();
const mouse = new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1,
-((event.clientY - rect.top) / rect.height) * 2 + 1
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, this.cameraManager.getCurrentCamera());
// Проверяем клики по интерактивным объектам интерьера
if (this.isInInterior) {
this.handleInteriorClick(raycaster);
}
// Проверяем клики по объектам города
this.handleCityClick(raycaster);
}
/**
* Обработка кликов по объектам интерьера
*/
handleInteriorClick(raycaster) {
const interactables = this.sceneManager.getInteriorInteractables();
const hits = raycaster.intersectObjects(interactables, true);
if (hits.length > 0) {
const hit = hits[0];
const payload = this.getInteractablePayload(hit.object);
if (payload) {
this.handleInteriorInteraction(payload);
}
}
}
/**
* Обработка кликов по объектам города
*/
handleCityClick(raycaster) {
const cityMeshes = this.sceneManager.cityMeshes;
const hits = raycaster.intersectObjects(cityMeshes, true);
if (hits.length > 0) {
const hit = hits[0];
const objectId = hit.object.userData.id;
if (objectId) {
this.handleCityObjectClick(objectId);
}
}
}
/**
* Получение данных интерактивного объекта
*/
getInteractablePayload(object) {
let node = object;
while (node && !node.userData?.payload && node.parent) {
node = node.parent;
}
return node?.userData?.payload;
}
/**
* Обработка взаимодействия с объектами интерьера
*/
handleInteriorInteraction(payload) {
switch (payload.type) {
case 'npc':
console.log('Взаимодействие с NPC:', payload.id);
// Здесь можно вызвать систему диалогов
break;
case 'marker':
console.log('Взаимодействие с маркером:', payload.label);
break;
default:
console.log('Неизвестный тип взаимодействия:', payload);
}
}
/**
* Обработка клика по объекту города
*/
async handleCityObjectClick(objectId) {
try {
const token = localStorage.getItem('token');
const response = await fetch(`/api/city_objects/${objectId}/interior`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
cache: 'no-cache'
});
if (response.ok) {
const { interiorId } = await response.json();
if (interiorId) {
await this.enterInterior(interiorId);
}
}
} catch (error) {
console.error('Ошибка при обработке клика по объекту города:', error);
}
}
/**
* Вход в интерьер
*/
async enterInterior(interiorId) {
try {
this.isInInterior = true;
this.playerManager.setInInterior(true);
// Сохраняем позицию игрока
const playerPosition = this.playerManager.getPlayerPosition();
this.playerManager.savePosition();
// Переключаемся на камеру от первого лица
this.cameraManager.switchToFirstPersonCamera(
playerPosition,
this.playerManager.getPlayerRotation()
);
// Загружаем интерьер
await this.interiorManager.enterInteriorMode(interiorId, playerPosition);
console.log('Вход в интерьер завершен');
} catch (error) {
console.error('Ошибка входа в интерьер:', error);
this.isInInterior = false;
this.playerManager.setInInterior(false);
}
}
/**
* Выход из интерьера
*/
exitInterior() {
if (!this.isInInterior) return;
try {
this.isInInterior = false;
this.playerManager.setInInterior(false);
// Восстанавливаем позицию игрока
this.playerManager.restorePosition();
// Переключаемся на ортографическую камеру
this.cameraManager.switchToOrthoCamera();
// Удаляем интерьер
this.sceneManager.removeInteriorGroup();
console.log('Выход из интерьера завершен');
} catch (error) {
console.error('Ошибка выхода из интерьера:', error);
}
}
/**
* Запуск игры
*/
start() {
this.isRunning = true;
this.gameLoop();
}
/**
* Остановка игры
*/
stop() {
this.isRunning = false;
}
/**
* Основной игровой цикл
*/
gameLoop() {
if (!this.isRunning) return;
const deltaTime = this.clock.getDelta();
// Обновляем игрока
this.playerManager.movePlayer(this.moveInput, deltaTime);
// Обновляем камеру
if (!this.isInInterior) {
const playerPosition = this.playerManager.getPlayerPosition();
this.cameraManager.updateOrthoCameraPosition(playerPosition);
} else {
const playerPosition = this.playerManager.getPlayerPosition();
const playerRotation = this.playerManager.getPlayerRotation();
this.cameraManager.updateFirstPersonCameraPosition(playerPosition, playerRotation);
}
// Рендерим сцену
this.rendererManager.render(
this.sceneManager.getScene(),
this.cameraManager.getCurrentCamera()
);
// Продолжаем цикл
requestAnimationFrame(() => this.gameLoop());
}
/**
* Получение менеджера сцены
*/
getSceneManager() {
return this.sceneManager;
}
/**
* Получение менеджера камер
*/
getCameraManager() {
return this.cameraManager;
}
/**
* Получение менеджера игрока
*/
getPlayerManager() {
return this.playerManager;
}
/**
* Получение менеджера рендерера
*/
getRendererManager() {
return this.rendererManager;
}
/**
* Получение менеджера интерьеров
*/
getInteriorManager() {
return this.interiorManager;
}
/**
* Получение менеджера коллизий
*/
getCollisionManager() {
return this.collisionManager;
}
/**
* Очистка ресурсов
*/
dispose() {
this.stop();
// Очищаем все модули
this.sceneManager.dispose();
this.cameraManager.dispose();
this.playerManager.dispose();
this.rendererManager.dispose();
this.interiorManager.dispose();
this.collisionManager.dispose();
// Удаляем обработчики событий
document.removeEventListener('keydown', this.handleKeyDown.bind(this));
document.removeEventListener('keyup', this.handleKeyUp.bind(this));
document.removeEventListener('mousemove', this.handleMouseMove.bind(this));
document.removeEventListener('wheel', this.handleWheel.bind(this));
window.removeEventListener('resize', this.handleResize.bind(this));
console.log('GameCore очищен');
}
}

View File

@@ -0,0 +1,358 @@
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
/**
* Менеджер интерьеров
* Отвечает за загрузку, управление и взаимодействие с интерьерами
*/
export class InteriorManager {
constructor(sceneManager) {
this.sceneManager = sceneManager;
this.loader = new GLTFLoader();
this.baseChairMesh = this.createBaseChairMesh();
this.texturePackCache = new Map();
this.cityPackMaterialCache = new Map();
this.init();
}
/**
* Инициализация менеджера интерьеров
*/
init() {
console.log('InteriorManager инициализирован');
}
/**
* Создание базового меша для стула
*/
createBaseChairMesh() {
return new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({ visible: false })
);
}
/**
* Загрузка GLTF модели
*/
async loadGLTF(url) {
return new Promise((resolve, reject) => {
this.loader.load(url, resolve, undefined, reject);
});
}
/**
* Вход в режим интерьера
*/
async enterInteriorMode(interiorId, playerPosition) {
console.log('Вход в интерьер:', interiorId);
// Сохраняем позицию игрока
if (playerPosition) {
this.sceneManager.savedPosition = playerPosition.clone();
}
// Загружаем модель интерьера
await this.loadInteriorModel(interiorId);
// Создаем группу интерьера
const interiorGroup = this.sceneManager.createInteriorGroup();
return interiorGroup;
}
/**
* Загрузка модели интерьера
*/
async loadInteriorModel(interiorId) {
console.log('Загрузка модели интерьера:', interiorId);
const token = localStorage.getItem('token');
if (!token) {
throw new Error('Токен не найден');
}
try {
// Получаем определение интерьера с сервера
const defRes = await fetch(`/api/interiors/${interiorId}/definition`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
cache: 'no-cache'
});
if (!defRes.ok) {
throw new Error(`Ошибка ${defRes.status} при загрузке определения интерьера`);
}
const { glb, objects } = await defRes.json();
const baseUrl = window.location.origin;
const glbUrl = baseUrl + glb;
console.log('Загрузка GLB из:', glbUrl);
// Проверяем доступность GLB файла
const headResp = await fetch(glbUrl, { method: 'HEAD', cache: 'no-cache' });
if (!headResp.ok) {
throw new Error(`GLB недоступен: HTTP ${headResp.status}`);
}
// Загружаем GLTF модель
const gltf = await this.loadGLTF(glbUrl);
const scene = this.sceneManager.getScene();
// Создаем группу для интерьера
const intGroup = new THREE.Group();
intGroup.name = 'interiorGroup';
intGroup.add(gltf.scene);
// Обрабатываем материалы интерьера
this.processInteriorMaterials(gltf.scene);
// Строим коллайдеры интерьера
this.buildInteriorColliders(gltf.scene);
// Добавляем объекты интерьера
await this.addInteriorObjects(objects, intGroup);
// Добавляем освещение для интерьера
this.addInteriorLighting(intGroup);
// Добавляем группу в сцену
scene.add(intGroup);
this.sceneManager.interiorGroup = intGroup;
console.log('Модель интерьера загружена успешно');
return intGroup;
} catch (error) {
console.error('Ошибка загрузки модели интерьера:', error);
throw error;
}
}
/**
* Обработка материалов интерьера
*/
processInteriorMaterials(scene) {
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;
}
}
});
}
/**
* Построение коллайдеров интерьера
*/
buildInteriorColliders(scene) {
const colliders = [];
scene.traverse((child) => {
if (child.isMesh && child.geometry) {
colliders.push(child);
}
});
this.sceneManager.interiorColliders = colliders;
}
/**
* Добавление объектов интерьера
*/
async addInteriorObjects(objects, intGroup) {
this.sceneManager.interiorInteractables = [];
for (const obj of objects) {
if (obj.model_url) {
try {
const objGltf = await this.loadGLTF(window.location.origin + obj.model_url);
objGltf.scene.position.set(obj.x, obj.y, obj.z);
objGltf.scene.rotation.set(obj.rot_x, obj.rot_y, obj.rot_z);
objGltf.scene.scale.set(obj.scale, obj.scale, obj.scale);
intGroup.add(objGltf.scene);
// Добавляем меши объекта как коллайдеры
objGltf.scene.traverse((child) => {
if (child.isMesh && child.geometry) {
this.sceneManager.interiorColliders.push(child);
}
});
// Обрабатываем NPC
if (this.isNPC(obj)) {
this.processNPC(obj, objGltf.scene, intGroup);
}
} catch (error) {
console.warn('Не удалось загрузить объект интерьера:', obj.model_url, error);
}
} else {
// Создаем плейсхолдер
const mesh = this.baseChairMesh.clone();
mesh.position.set(obj.x, obj.y, obj.z);
mesh.rotation.set(obj.rot_x, obj.rot_y, obj.rot_z);
mesh.scale.set(obj.scale, obj.scale, obj.scale);
intGroup.add(mesh);
mesh.visible = false;
this.sceneManager.interiorColliders.push(mesh);
}
// Обрабатываем интерактивные маркеры
if (obj.interactable || obj.marker) {
this.createInteriorMarker(obj, intGroup);
}
// Сохраняем позицию внутреннего выхода
if (obj.exit_int_x !== undefined && obj.exit_int_y !== undefined && obj.exit_int_z !== undefined) {
this.sceneManager.setInteriorExitPos(new THREE.Vector3(obj.exit_int_x, obj.exit_int_y, obj.exit_int_z));
}
}
}
/**
* Проверка, является ли объект NPC
*/
isNPC(obj) {
return (obj.type === 'npc') ||
(typeof obj.model_url === 'string' && obj.model_url.includes('/models/npc/'));
}
/**
* Обработка NPC
*/
processNPC(obj, scene, intGroup) {
const npcId = obj.id || this.getNpcIdFromModel(obj.model_url);
console.log('Обнаружен NPC:', npcId, 'в позиции:', { x: obj.x, y: obj.y, z: obj.z });
// Создаем хит-зону для NPC
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(obj.x, (obj.y ?? 0) + 1.0, obj.z);
hit.userData.interactable = true;
hit.userData.payload = { type: 'npc', id: npcId };
hit.visible = true;
intGroup.add(hit);
this.sceneManager.interiorInteractables.push(hit);
// Помечаем корень модели как кликабельный NPC
try {
scene.userData = scene.userData || {};
scene.userData.interactable = true;
scene.userData.payload = { type: 'npc', id: npcId };
scene.userData.isNpc = true;
scene.userData.npcId = npcId;
this.sceneManager.interiorInteractables.push(scene);
} catch (error) {
console.warn('Ошибка при обработке NPC:', error);
}
}
/**
* Получение ID NPC из пути к модели
*/
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;
}
/**
* Создание интерактивного маркера
*/
createInteriorMarker(obj, intGroup) {
const hit = new THREE.Mesh(
new THREE.SphereGeometry(0.6),
new THREE.MeshBasicMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.0001,
depthWrite: false
})
);
hit.position.set(obj.x, obj.y + 1.0, obj.z);
hit.userData.interactable = true;
hit.userData.payload = {
type: obj.type || 'marker',
id: obj.id || null,
label: obj.label || 'Интерактив'
};
hit.visible = true;
try {
if (hit.material) hit.material.visible = false;
} catch (error) {
// Игнорируем ошибки
}
intGroup.add(hit);
this.sceneManager.interiorInteractables.push(hit);
}
/**
* Добавление освещения для интерьера
*/
addInteriorLighting(intGroup) {
const light = new THREE.AmbientLight(0xffffff, 1);
intGroup.add(light);
}
/**
* Выход из интерьера
*/
exitInterior(playerPosition, exitPosition) {
console.log('Выход из интерьера');
// Телепортируем игрока
if (playerPosition && exitPosition) {
playerPosition.set(
exitPosition.x,
typeof exitPosition.y === 'number' ? exitPosition.y : playerPosition.y,
exitPosition.z
);
}
// Удаляем группу интерьера
this.sceneManager.removeInteriorGroup();
// Возвращаем видимость мира
this.sceneManager.toggleWorldVisibility(true);
}
/**
* Очистка ресурсов
*/
dispose() {
this.texturePackCache.clear();
this.cityPackMaterialCache.clear();
}
}

View File

@@ -0,0 +1,163 @@
import * as THREE from 'three';
/**
* Менеджер игрока
* Отвечает за создание, управление и анимацию игрока
*/
export class PlayerManager {
constructor(sceneManager, collisionManager = null) {
this.sceneManager = sceneManager;
this.collisionManager = collisionManager;
this.player = null;
this.mixer = null;
this.moveSpeed = 2.5;
this.interiorMoveSpeed = 3.0; // Скорость движения в интерьере
this.savedPosition = new THREE.Vector3();
this.remotePlayers = {};
this.isInInterior = false;
this.init();
}
/**
* Инициализация игрока
*/
async init() {
await this.createPlayer();
}
/**
* Создание модели игрока
*/
async createPlayer() {
try {
// Создаем простую геометрию для игрока
const geometry = new THREE.BoxGeometry(1, 2, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x0000ff });
this.player = new THREE.Mesh(geometry, material);
this.player.position.set(0, 0, 0);
this.player.castShadow = true;
// Добавляем игрока на сцену
this.sceneManager.getScene().add(this.player);
console.log('Игрок создан успешно');
} catch (error) {
console.error('Ошибка создания игрока:', error);
}
}
/**
* Движение игрока
*/
movePlayer(direction, deltaTime) {
if (!this.player) return;
const moveDistance = this.isInInterior ? this.interiorMoveSpeed * deltaTime : this.moveSpeed * deltaTime;
const moveVector = new THREE.Vector3();
if (direction.forward) moveVector.z -= moveDistance;
if (direction.backward) moveVector.z += moveDistance;
if (direction.left) moveVector.x -= moveDistance;
if (direction.right) moveVector.x += moveDistance;
// Если игрок в интерьере, используем систему коллизий
if (this.isInInterior && this.collisionManager) {
const targetPosition = this.player.position.clone().add(moveVector);
const safePosition = this.collisionManager.checkInteriorCollisions(
this.player.position,
targetPosition,
moveVector,
deltaTime
);
this.player.position.copy(safePosition);
} else {
// Обычное движение без коллизий
this.player.position.add(moveVector);
}
// Поворачиваем игрока в направлении движения
if (moveVector.length() > 0) {
const angle = Math.atan2(moveVector.x, moveVector.z);
this.player.rotation.y = angle;
}
}
/**
* Телепортация игрока
*/
teleportPlayer(position, rotation = null) {
if (!this.player) return;
// Если игрок в интерьере, проверяем безопасность позиции
if (this.isInInterior && this.collisionManager) {
const safePosition = this.collisionManager.getSafeTeleportPosition(position);
this.player.position.copy(safePosition);
} else {
this.player.position.copy(position);
}
if (rotation !== null) {
this.player.rotation.y = rotation;
}
}
/**
* Сохранение позиции игрока
*/
savePosition() {
if (this.player) {
this.savedPosition.copy(this.player.position);
}
}
/**
* Восстановление позиции игрока
*/
restorePosition() {
if (this.player && this.savedPosition.length() > 0) {
this.player.position.copy(this.savedPosition);
}
}
/**
* Установка состояния интерьера
*/
setInInterior(inInterior) {
this.isInInterior = inInterior;
}
/**
* Получение поворота игрока
*/
getPlayerRotation() {
return this.player ? this.player.rotation.clone() : new THREE.Euler();
}
/**
* Получение позиции игрока
*/
getPlayerPosition() {
return this.player ? this.player.position.clone() : new THREE.Vector3();
}
/**
* Получение объекта игрока
*/
getPlayer() {
return this.player;
}
/**
* Очистка ресурсов
*/
dispose() {
if (this.player) {
this.sceneManager.getScene().remove(this.player);
}
this.player = null;
this.remotePlayers = {};
}
}

90
src/modules/README.md Normal file
View File

@@ -0,0 +1,90 @@
# Модульная структура проекта EEV_Proj
## Обзор
Проект разбит на логические модули для улучшения читаемости, поддерживаемости и переиспользования кода.
## Структура модулей
### 1. Core (Ядро)
- **GameCore.js** - Основная логика игры
- **SceneManager.js** - Управление 3D сценой
- **CameraManager.js** - Управление камерами
- **PlayerManager.js** - Управление игроком
- **CollisionManager.js** - Система коллизий для интерьеров
### 2. Rendering (Рендеринг)
- **RendererManager.js** - Управление рендерером
- **MaterialManager.js** - Управление материалами
- **TextureManager.js** - Управление текстурами
- **ModelLoader.js** - Загрузка 3D моделей
### 3. Gameplay (Игровой процесс)
- **InteriorManager.js** - Управление интерьерами
- **DialogManager.js** - Система диалогов
- **InventoryManager.js** - Управление инвентарем
- **QuestManager.js** - Система квестов
### 4. UI (Пользовательский интерфейс)
- **PhoneManager.js** - Виртуальный телефон
- **AppManager.js** - Управление приложениями
- **NotificationManager.js** - Система уведомлений
### 5. Networking (Сетевое взаимодействие)
- **SocketManager.js** - Управление WebSocket соединениями
- **VoiceChatManager.js** - Голосовой чат
- **MessageManager.js** - Система сообщений
### 6. Utils (Утилиты)
- **Pathfinding.js** - Поиск пути
- **CollisionDetection.js** - Обнаружение коллизий
- **AnimationManager.js** - Управление анимациями
## Система коллизий для интерьеров
### CollisionManager.js
Модуль отвечает за проверку столкновений игрока с объектами интерьера при движении от первого лица.
**Основные функции:**
- `checkInteriorCollisions()` - проверка коллизий при движении
- `checkPlayerCollision()` - проверка столкновения с конкретным объектом
- `getSafeTeleportPosition()` - поиск безопасной позиции для телепортации
- `checkInteriorInteractions()` - проверка взаимодействий с интерактивными объектами
**Особенности:**
- Использует AABB (Axis-Aligned Bounding Box) для быстрой проверки коллизий
- Кэширование коллайдеров для оптимизации производительности
- Раздельная проверка по осям X и Z для плавного движения
- Автоматическое исключение интерактивных объектов и хит-зон
**Интеграция:**
- Работает совместно с `PlayerManager` для ограничения движения
- Использует данные из `SceneManager` о коллайдерах интерьера
- Поддерживает различные типы геометрии (Box, Sphere, Custom)
## Принципы модулизации
1. **Единая ответственность** - каждый модуль отвечает за одну область функциональности
2. **Слабая связанность** - модули минимально зависят друг от друга
3. **Высокая когезия** - связанные функции находятся в одном модуле
4. **Интерфейсы** - четко определенные API между модулями
5. **Переиспользование** - модули можно использовать в других частях проекта
## Импорты и экспорты
```javascript
// Пример импорта
import { SceneManager } from './modules/SceneManager.js';
import { CameraManager } from './modules/CameraManager.js';
// Пример экспорта
export class GameCore {
// ...
}
```
## Конфигурация
Каждый модуль может иметь свой конфигурационный файл:
- `config.js` - основные настройки
- `constants.js` - константы
- `types.js` - типы данных (для TypeScript)

View File

@@ -0,0 +1,132 @@
import * as THREE from 'three';
/**
* Менеджер рендерера
* Отвечает за создание, настройку и управление WebGL рендерером
*/
export class RendererManager {
constructor(container) {
this.container = container;
this.renderer = null;
this.isInitialized = false;
this.init();
}
/**
* Инициализация рендерера
*/
init() {
try {
this.createRenderer();
this.setupRenderer();
this.addToContainer();
this.isInitialized = true;
console.log('Рендерер инициализирован успешно');
} catch (error) {
console.error('Ошибка инициализации рендерера:', error);
}
}
/**
* Создание WebGL рендерера
*/
createRenderer() {
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
powerPreference: "high-performance"
});
}
/**
* Настройка рендерера
*/
setupRenderer() {
if (!this.renderer) return;
// Настройка размера
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Настройка теней
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Настройка цвета фона
this.renderer.setClearColor(0x87ceeb);
// Настройка гамма
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.0;
}
/**
* Добавление рендерера в контейнер
*/
addToContainer() {
if (this.container && this.renderer) {
this.container.appendChild(this.renderer.domElement);
}
}
/**
* Рендеринг сцены
*/
render(scene, camera) {
if (this.renderer && scene && camera) {
this.renderer.render(scene, camera);
}
}
/**
* Обработка изменения размера окна
*/
handleResize() {
if (!this.renderer) return;
const width = window.innerWidth;
const height = window.innerHeight;
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
/**
* Получение DOM элемента рендерера
*/
getDomElement() {
return this.renderer ? this.renderer.domElement : null;
}
/**
* Получение рендерера
*/
getRenderer() {
return this.renderer;
}
/**
* Проверка инициализации
*/
isReady() {
return this.isInitialized && this.renderer !== null;
}
/**
* Очистка ресурсов
*/
dispose() {
if (this.renderer) {
this.renderer.dispose();
if (this.container && this.renderer.domElement) {
this.container.removeChild(this.renderer.domElement);
}
}
this.renderer = null;
this.isInitialized = false;
}
}

212
src/modules/SceneManager.js Normal file
View File

@@ -0,0 +1,212 @@
import * as THREE from 'three';
/**
* Менеджер 3D сцены
* Отвечает за создание, управление и обновление 3D сцены
*/
export class SceneManager {
constructor() {
this.scene = new THREE.Scene();
this.cityGroup = new THREE.Group();
this.interiorGroup = null;
this.ground = null;
this.cityMeshes = [];
this.cityObjectsData = [];
this.loadedCityObjects = {};
this.loadedInteriorMeshes = {};
this.interiorsData = [];
this.npcMeshes = [];
this.interiorColliders = [];
this.interiorInteractables = [];
this.interiorExitPos = null;
this.fpHiddenNodes = [];
this.cleanupTimer = null;
this.overlayTimeout = null;
this.init();
}
/**
* Инициализация сцены
*/
init() {
this.scene.add(this.cityGroup);
this.setupLighting();
this.setupGround();
}
/**
* Настройка освещения сцены
*/
setupLighting() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(100, 100, 50);
directionalLight.castShadow = true;
this.scene.add(directionalLight);
}
/**
* Создание поверхности земли
*/
setupGround() {
const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x3a5f3a,
roughness: 0.8,
metalness: 0.1
});
this.ground = new THREE.Mesh(groundGeometry, groundMaterial);
this.ground.rotation.x = -Math.PI / 2;
this.ground.receiveShadow = true;
this.scene.add(this.ground);
}
/**
* Добавление объекта в город
*/
addCityObject(mesh, data) {
this.cityGroup.add(mesh);
this.cityMeshes.push(mesh);
this.cityObjectsData.push(data);
}
/**
* Удаление объекта из города
*/
removeCityObject(mesh) {
this.cityGroup.remove(mesh);
const index = this.cityMeshes.indexOf(mesh);
if (index > -1) {
this.cityMeshes.splice(index, 1);
this.cityObjectsData.splice(index, 1);
}
}
/**
* Создание группы интерьера
*/
createInteriorGroup() {
this.interiorGroup = new THREE.Group();
this.interiorGroup.name = 'interiorGroup';
this.scene.add(this.interiorGroup);
return this.interiorGroup;
}
/**
* Удаление группы интерьера
*/
removeInteriorGroup() {
if (this.interiorGroup) {
this.scene.remove(this.interiorGroup);
this.interiorGroup = null;
this.interiorColliders = [];
this.interiorInteractables = [];
this.interiorExitPos = null;
}
}
/**
* Добавление коллайдера интерьера
*/
addInteriorCollider(mesh) {
this.interiorColliders.push(mesh);
}
/**
* Добавление интерактивного объекта интерьера
*/
addInteriorInteractable(mesh) {
this.interiorInteractables.push(mesh);
}
/**
* Установка позиции выхода из интерьера
*/
setInteriorExitPos(position) {
this.interiorExitPos = position;
}
/**
* Переключение видимости мира
*/
toggleWorldVisibility(visible) {
if (this.ground) this.ground.visible = visible;
this.cityMeshes.forEach(mesh => mesh.visible = visible);
}
/**
* Очистка ресурсов
*/
dispose() {
// Очистка таймеров
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
if (this.overlayTimeout) {
clearTimeout(this.overlayTimeout);
}
// Очистка геометрий и материалов
this.scene.traverse((object) => {
if (object.geometry) {
object.geometry.dispose();
}
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(material => material.dispose());
} else {
object.material.dispose();
}
}
});
// Очистка сцены
this.scene.clear();
}
/**
* Получение сцены
*/
getScene() {
return this.scene;
}
/**
* Получение группы города
*/
getCityGroup() {
return this.cityGroup;
}
/**
* Получение группы интерьера
*/
getInteriorGroup() {
return this.interiorGroup;
}
/**
* Получение коллайдеров интерьера
*/
getInteriorColliders() {
return this.interiorColliders;
}
/**
* Получение интерактивных объектов интерьера
*/
getInteriorInteractables() {
return this.interiorInteractables;
}
/**
* Получение позиции выхода из интерьера
*/
getInteriorExitPos() {
return this.interiorExitPos;
}
}

View File

@@ -0,0 +1,419 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
export default function CollisionEditor() {
const mountRef = useRef(null);
const sceneRef = useRef();
const cameraRef = useRef();
const rendererRef = useRef();
const orbitRef = useRef();
const transformRef = useRef();
const backgroundGroupRef = useRef(new THREE.Group());
const gltfLoaderRef = useRef(new GLTFLoader());
const [shapeType, setShapeType] = useState('box');
const [mode, setMode] = useState('translate');
const [selected, setSelected] = useState(null);
const [cursorXZ, setCursorXZ] = useState({ x: 0, z: 0 });
const [cities, setCities] = useState([]);
const [cityId, setCityId] = useState(null);
const [lockUniformXZ, setLockUniformXZ] = useState(true);
const collidersRef = useRef([]);
const colliderMaterial = useMemo(() => new THREE.MeshBasicMaterial({ color: 0x00aaff, transparent: true, opacity: 0.25, depthWrite: false }), []);
const colliderEdgeMaterial = useMemo(() => new THREE.LineBasicMaterial({ color: 0x00aaff }), []);
useEffect(() => {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x9aa7b1);
sceneRef.current = scene;
const camera = new THREE.PerspectiveCamera(60, mountRef.current.clientWidth / mountRef.current.clientHeight, 0.1, 2000);
camera.position.set(20, 20, 20);
camera.lookAt(0, 0, 0);
cameraRef.current = camera;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(mountRef.current.clientWidth, mountRef.current.clientHeight);
mountRef.current.appendChild(renderer.domElement);
rendererRef.current = renderer;
const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 0.9);
hemi.position.set(0, 50, 0);
scene.add(hemi);
const grid = new THREE.GridHelper(1000, 100);
scene.add(grid);
scene.add(backgroundGroupRef.current);
const orbit = new OrbitControls(camera, renderer.domElement);
orbit.enableDamping = true;
orbitRef.current = orbit;
const transform = new TransformControls(camera, renderer.domElement);
transform.addEventListener('dragging-changed', e => {
orbit.enabled = !e.value;
});
transform.addEventListener('change', () => {
renderer.render(scene, camera);
});
scene.add(transform);
transformRef.current = transform;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const onPointerDown = (event) => {
if (transform.dragging) return;
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const meshes = collidersRef.current.map(c => c.mesh);
const hits = raycaster.intersectObjects(meshes, true);
if (hits.length > 0) {
let obj = hits[0].object;
while (obj && !meshes.includes(obj) && obj.parent) obj = obj.parent;
if (obj) {
setSelected(obj);
transform.attach(obj);
}
} else {
setSelected(null);
transform.detach();
}
};
renderer.domElement.addEventListener('pointerdown', onPointerDown);
const onPointerMove = (event) => {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const p = new THREE.Vector3();
raycaster.ray.intersectPlane(groundPlane, p);
if (isFinite(p.x) && isFinite(p.z)) setCursorXZ({ x: p.x, z: p.z });
};
renderer.domElement.addEventListener('pointermove', onPointerMove);
const onKeyDown = (e) => {
// Перемещение целевой точки орбиты стрелками
const step = e.shiftKey ? 2 : 0.5;
if (!orbitRef.current) return;
const tgt = orbitRef.current.target;
if (e.key === 'ArrowUp') { tgt.z -= step; e.preventDefault(); }
if (e.key === 'ArrowDown') { tgt.z += step; e.preventDefault(); }
if (e.key === 'ArrowLeft') { tgt.x -= step; e.preventDefault(); }
if (e.key === 'ArrowRight') { tgt.x += step; e.preventDefault(); }
orbitRef.current.update();
};
window.addEventListener('keydown', onKeyDown);
const onResize = () => {
if (!mountRef.current) return;
camera.aspect = mountRef.current.clientWidth / mountRef.current.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(mountRef.current.clientWidth, mountRef.current.clientHeight);
};
window.addEventListener('resize', onResize);
const animate = () => {
requestAnimationFrame(animate);
orbit.update();
renderer.render(scene, camera);
};
animate();
return () => {
window.removeEventListener('resize', onResize);
window.removeEventListener('keydown', onKeyDown);
renderer.domElement.removeEventListener('pointerdown', onPointerDown);
renderer.domElement.removeEventListener('pointermove', onPointerMove);
mountRef.current && mountRef.current.removeChild(renderer.domElement);
};
}, []);
useEffect(() => {
transformRef.current?.setMode(mode);
}, [mode]);
useEffect(() => {
const token = localStorage.getItem('token');
fetch('/api/cities', { headers: { Authorization: `Bearer ${token}` } })
.then(r => r.json())
.then(data => {
setCities(data);
const profile = JSON.parse(sessionStorage.getItem('user_profile') || '{}');
const defaultCity = profile.last_city_id || data[0]?.id;
setCityId(defaultCity || null);
})
.catch(() => {});
}, []);
useEffect(() => {
if (!cityId || !sceneRef.current) return;
const token = localStorage.getItem('token');
const bg = backgroundGroupRef.current;
while (bg.children.length) {
const ch = bg.children.pop();
ch.traverse(n => {
if (n.isMesh) {
n.geometry?.dispose?.();
if (n.material) {
if (Array.isArray(n.material)) n.material.forEach(m => m.dispose?.());
else n.material.dispose?.();
}
}
});
}
fetch(`/api/cities/${cityId}/objects`, { headers: { Authorization: `Bearer ${token}` } })
.then(r => r.json())
.then(async data => {
for (const obj of data) {
try {
const gltf = await gltfLoaderRef.current.loadAsync(obj.model_url);
const m = gltf.scene;
m.position.set(obj.pos_x, obj.pos_y, obj.pos_z);
m.rotation.set(obj.rot_x, obj.rot_y, obj.rot_z);
m.scale.set(obj.scale_x || 1, obj.scale_y || 1, obj.scale_z || 1);
m.traverse(child => {
if (child.isMesh && child.material) {
if (Array.isArray(child.material)) child.material.forEach(mat => { if (mat) mat.transparent = true; if (mat) mat.opacity = 0.9; });
else { child.material.transparent = true; child.material.opacity = 0.9; }
child.raycast = () => {};
}
});
bg.add(m);
} catch (e) {}
}
})
.catch(() => {});
}, [cityId]);
// Автозагрузка коллизий из API
useEffect(() => {
const token = localStorage.getItem('token');
const q = cityId ? `?cityId=${encodeURIComponent(cityId)}` : '';
fetch(`/api/colliders${q}`, { headers: { 'Authorization': `Bearer ${token}` } })
.then(r => r.json())
.then(data => {
collidersRef.current.forEach(c => sceneRef.current.remove(c.mesh));
collidersRef.current = [];
const list = Array.isArray(data?.colliders) ? data.colliders : [];
list.forEach(c => {
let geom;
if (c.type === 'circle') geom = new THREE.CylinderGeometry(1.5, 1.5, 2, 32);
else if (c.type === 'capsule') geom = new THREE.CapsuleGeometry(1, 2, 4, 12);
else geom = new THREE.BoxGeometry(2, 2, 2);
const mesh = new THREE.Mesh(geom, colliderMaterial.clone());
const edges = new THREE.EdgesGeometry(mesh.geometry);
const line = new THREE.LineSegments(edges, colliderEdgeMaterial.clone());
mesh.add(line);
mesh.position.set(c.position?.x || 0, c.position?.y || 0, c.position?.z || 0);
mesh.rotation.set(c.rotation?.x || 0, c.rotation?.y || 0, c.rotation?.z || 0);
mesh.scale.set(c.scale?.x || 1, c.scale?.y || 1, c.scale?.z || 1);
mesh.userData = { type: c.type || 'box' };
sceneRef.current.add(mesh);
collidersRef.current.push({ mesh });
});
})
.catch(() => {});
}, [cityId]);
// Авто-сохранение (дебаунс) при изменениях
const saveTimer = useRef(null);
const requestSave = () => {
clearTimeout(saveTimer.current);
saveTimer.current = setTimeout(() => {
const token = localStorage.getItem('token');
const payload = { colliders: serializeColliders(), cityId };
fetch('/api/colliders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
}).catch(() => {});
}, 400);
};
const addCollider = () => {
if (!sceneRef.current) return;
let mesh;
if (shapeType === 'box') {
const geom = new THREE.BoxGeometry(2, 2, 2);
mesh = new THREE.Mesh(geom, colliderMaterial.clone());
} else if (shapeType === 'circle') {
const geom = new THREE.CylinderGeometry(1.5, 1.5, 2, 32);
mesh = new THREE.Mesh(geom, colliderMaterial.clone());
} else if (shapeType === 'capsule') {
const geom = new THREE.CapsuleGeometry(1, 2, 4, 12);
mesh = new THREE.Mesh(geom, colliderMaterial.clone());
} else {
const geom = new THREE.BoxGeometry(2, 2, 2);
mesh = new THREE.Mesh(geom, colliderMaterial.clone());
}
const edges = new THREE.EdgesGeometry(mesh.geometry);
const line = new THREE.LineSegments(edges, colliderEdgeMaterial.clone());
mesh.add(line);
mesh.position.set(0, 1, 0);
mesh.userData = { type: shapeType };
sceneRef.current.add(mesh);
collidersRef.current.push({ mesh });
transformRef.current.attach(mesh);
setSelected(mesh);
requestSave();
};
const deleteSelected = () => {
if (!selected) return;
transformRef.current.detach();
sceneRef.current.remove(selected);
collidersRef.current = collidersRef.current.filter(c => c.mesh !== selected);
setSelected(null);
requestSave();
};
const serializeColliders = () => {
return collidersRef.current.map(({ mesh }) => {
const type = mesh.userData?.type || 'box';
return {
type,
position: { x: mesh.position.x, y: mesh.position.y, z: mesh.position.z },
rotation: { x: mesh.rotation.x, y: mesh.rotation.y, z: mesh.rotation.z },
scale: { x: mesh.scale.x, y: mesh.scale.y, z: mesh.scale.z }
};
});
};
const exportJSON = () => {
const data = serializeColliders();
const blob = new Blob([JSON.stringify({ colliders: data }, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'colliders.json';
a.click();
URL.revokeObjectURL(url);
};
const importJSON = (file) => {
const reader = new FileReader();
reader.onload = () => {
try {
const parsed = JSON.parse(reader.result);
collidersRef.current.forEach(c => sceneRef.current.remove(c.mesh));
collidersRef.current = [];
if (Array.isArray(parsed?.colliders)) {
parsed.colliders.forEach(c => {
let geom;
if (c.type === 'circle') geom = new THREE.CylinderGeometry(1.5, 1.5, 2, 32);
else if (c.type === 'capsule') geom = new THREE.CapsuleGeometry(1, 2, 4, 12);
else geom = new THREE.BoxGeometry(2, 2, 2);
const mesh = new THREE.Mesh(geom, colliderMaterial.clone());
const edges = new THREE.EdgesGeometry(mesh.geometry);
const line = new THREE.LineSegments(edges, colliderEdgeMaterial.clone());
mesh.add(line);
mesh.position.set(c.position?.x || 0, c.position?.y || 0, c.position?.z || 0);
mesh.rotation.set(c.rotation?.x || 0, c.rotation?.y || 0, c.rotation?.z || 0);
mesh.scale.set(c.scale?.x || 1, c.scale?.y || 1, c.scale?.z || 1);
mesh.userData = { type: c.type || 'box' };
sceneRef.current.add(mesh);
collidersRef.current.push({ mesh });
});
requestSave();
}
} catch (e) {
alert('Некорректный JSON');
}
};
reader.readAsText(file);
};
const setSelectedPosition = (axis, value) => {
if (!selected) return;
const v = parseFloat(value);
if (!isFinite(v)) return;
selected.position[axis] = v;
requestSave();
};
const setSelectedScale = (axis, value) => {
if (!selected) return;
const v = Math.max(0.01, parseFloat(value));
if (!isFinite(v)) return;
if (lockUniformXZ && (axis === 'x' || axis === 'z')) {
selected.scale.x = v;
selected.scale.z = v;
} else {
selected.scale[axis] = v;
}
requestSave();
};
return (
<div style={{ width: '100%', height: '100vh', position: 'relative' }} ref={mountRef}>
<div style={{ position: 'absolute', top: 10, left: 10, background: 'rgba(255,255,255,0.9)', padding: 8, display: 'grid', gap: 8, minWidth: 320 }}>
<div>
<label>Город:&nbsp;</label>
<select value={cityId || ''} onChange={e => setCityId(Number(e.target.value))}>
{cities.map(c => (
<option key={c.id} value={c.id}>{c.name} ({c.country_name})</option>
))}
</select>
</div>
<div>
<label>Форма:&nbsp;</label>
<select value={shapeType} onChange={e => setShapeType(e.target.value)}>
<option value="box">Прямоугольник</option>
<option value="circle">Круг (цилиндр)</option>
<option value="capsule">Капсула</option>
</select>
<button onClick={addCollider} style={{ marginLeft: 8 }}>Добавить</button>
</div>
<div>
<button onClick={() => setMode(mode === 'translate' ? 'rotate' : mode === 'rotate' ? 'scale' : 'translate')}>
Режим: {mode === 'translate' ? 'Перемещение' : mode === 'rotate' ? 'Вращение' : 'Масштаб'}
</button>
<button onClick={deleteSelected} style={{ marginLeft: 8 }}>Удалить выделенный</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, auto)', gap: 6, alignItems: 'center' }}>
<b>Позиция</b>
<span>X</span><input type="number" step="0.1" value={selected ? selected.position.x : ''} onChange={e => setSelectedPosition('x', e.target.value)} />
<span>Y</span><input type="number" step="0.1" value={selected ? selected.position.y : ''} onChange={e => setSelectedPosition('y', e.target.value)} />
<span>Z</span><input type="number" step="0.1" value={selected ? selected.position.z : ''} onChange={e => setSelectedPosition('z', e.target.value)} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, auto)', gap: 6, alignItems: 'center' }}>
<b>Размер</b>
<span>X</span><input type="number" min="0.01" step="0.1" value={selected ? selected.scale.x : ''} onChange={e => setSelectedScale('x', e.target.value)} />
<span>Y</span><input type="number" min="0.01" step="0.1" value={selected ? selected.scale.y : ''} onChange={e => setSelectedScale('y', e.target.value)} />
<span>Z</span><input type="number" min="0.01" step="0.1" value={selected ? selected.scale.z : ''} onChange={e => setSelectedScale('z', e.target.value)} />
<label style={{ marginLeft: 6 }}>
<input type="checkbox" checked={lockUniformXZ} onChange={e => setLockUniformXZ(e.target.checked)} /> XZ равны
</label>
</div>
<div>
<b>Курсор</b>: X {cursorXZ.x.toFixed(2)} | Z {cursorXZ.z.toFixed(2)}
</div>
<div>
<button onClick={exportJSON}>Экспорт JSON</button>
<label style={{ marginLeft: 8 }}>
Импорт JSON
<input type="file" accept="application/json" style={{ display: 'block' }} onChange={(e) => e.target.files && e.target.files[0] && importJSON(e.target.files[0])} />
</label>
</div>
<div>
Подсказка: стрелки двигают точку прицеливания камеры (Shift быстрее)
</div>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

454
src/pages/QuestSystem .jsx Normal file
View File

@@ -0,0 +1,454 @@
import React, { useState, useEffect } from 'react';
const QuestSystem = ({ onClose }) => {
const [quests, setQuests] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedQuest, setSelectedQuest] = useState(null);
const [activeTab, setActiveTab] = useState('available');
// Загрузка квестов при монтировании компонента
useEffect(() => {
loadQuests();
}, []);
// Функция загрузки квестов с сервера
const loadQuests = async () => {
try {
setLoading(true);
const token = localStorage.getItem('token');
const response = await fetch('/api/quests/player-status', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Ошибка загрузки: ${response.status}`);
}
const data = await response.json();
if (data.success) {
setQuests(data.quests || []);
} else {
throw new Error(data.error || 'Неизвестная ошибка сервера');
}
} catch (err) {
console.error('Ошибка загрузки квестов:', err);
setError(err.message);
} finally {
setLoading(false);
}
};
// Функция начала квеста
const startQuest = async (questId) => {
try {
const token = localStorage.getItem('token');
const response = await fetch(`/api/quests/${questId}/start`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Ошибка начала квеста: ${response.status}`);
}
const data = await response.json();
if (data.success) {
await loadQuests();
} else {
throw new Error(data.error || 'Неизвестная ошибка сервера');
}
} catch (err) {
console.error('Ошибка начала квеста:', err);
alert(`Ошибка начала квеста: ${err.message}`);
}
};
// Фильтрация квестов по статусу
const filteredQuests = quests.filter(quest => {
switch (activeTab) {
case 'available':
return quest.status === 'available' && quest.hasAccess;
case 'in_progress':
return quest.status === 'in_progress';
case 'completed':
return quest.status === 'completed';
case 'locked':
return quest.status === 'locked' || !quest.hasAccess;
default:
return true;
}
});
// Стили компонента
const styles = {
container: {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '80%',
maxWidth: '800px',
maxHeight: '80vh',
backgroundColor: 'rgba(0, 0, 0, 0.95)',
border: '2px solid #444',
borderRadius: '12px',
padding: '20px',
zIndex: 10000,
color: 'white',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
},
header: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
borderBottom: '1px solid #444',
paddingBottom: '10px'
},
closeButton: {
background: 'transparent',
border: 'none',
color: 'white',
fontSize: '24px',
cursor: 'pointer',
padding: '5px 10px'
},
tabs: {
display: 'flex',
marginBottom: '20px',
borderBottom: '1px solid #444'
},
tab: {
padding: '10px 20px',
cursor: 'pointer',
border: 'none',
background: 'transparent',
color: '#aaa',
borderBottom: '2px solid transparent',
transition: 'all 0.3s'
},
activeTab: {
color: 'white',
borderBottom: '2px solid #4CAF50'
},
questList: {
flex: 1,
overflowY: 'auto',
paddingRight: '10px'
},
questItem: {
background: 'rgba(50, 50, 50, 0.7)',
borderRadius: '8px',
padding: '15px',
marginBottom: '10px',
border: '1px solid #444',
cursor: 'pointer',
transition: 'all 0.3s'
},
questHeader: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '10px'
},
questTitle: {
fontSize: '18px',
fontWeight: 'bold',
margin: 0
},
questStatus: {
padding: '3px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold'
},
statusAvailable: {
backgroundColor: '#4CAF50',
color: 'white'
},
statusInProgress: {
backgroundColor: '#2196F3',
color: 'white'
},
statusCompleted: {
backgroundColor: '#9C27B0',
color: 'white'
},
statusLocked: {
backgroundColor: '#757575',
color: 'white'
},
questDescription: {
margin: '10px 0',
color: '#ccc',
fontSize: '14px'
},
questActions: {
display: 'flex',
gap: '10px',
marginTop: '10px'
},
button: {
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 'bold',
transition: 'all 0.3s'
},
startButton: {
backgroundColor: '#4CAF50',
color: 'white'
},
viewButton: {
backgroundColor: '#2196F3',
color: 'white'
},
disabledButton: {
backgroundColor: '#757575',
color: '#aaa',
cursor: 'not-allowed'
},
questDetail: {
marginTop: '20px',
padding: '15px',
background: 'rgba(40, 40, 40, 0.8)',
borderRadius: '8px',
border: '1px solid #444'
},
stepList: {
marginTop: '15px'
},
stepItem: {
padding: '10px',
marginBottom: '5px',
background: 'rgba(60, 60, 60, 0.6)',
borderRadius: '4px',
borderLeft: '3px solid #444'
},
currentStep: {
borderLeft: '3px solid #4CAF50',
background: 'rgba(76, 175, 80, 0.1)'
},
completedStep: {
borderLeft: '3px solid #9C27B0',
background: 'rgba(156, 39, 176, 0.1)'
},
loading: {
textAlign: 'center',
padding: '20px',
color: '#aaa'
},
error: {
textAlign: 'center',
padding: '20px',
color: '#f44336'
}
};
// Функция для получения стиля статуса
const getStatusStyle = (status) => {
switch (status) {
case 'available': return { ...styles.questStatus, ...styles.statusAvailable };
case 'in_progress': return { ...styles.questStatus, ...styles.statusInProgress };
case 'completed': return { ...styles.questStatus, ...styles.statusCompleted };
case 'locked': return { ...styles.questStatus, ...styles.statusLocked };
default: return styles.questStatus;
}
};
// Функция для получения текста статуса
const getStatusText = (status) => {
switch (status) {
case 'available': return 'Доступен';
case 'in_progress': return 'В процессе';
case 'completed': return 'Завершен';
case 'locked': return 'Заблокирован';
default: return status;
}
};
return (
<div style={styles.container}>
{/* ЗДЕСЬ КНОПКА ЗАКРЫТИЯ - внутри header */}
<div style={styles.header}>
<h2 style={{ margin: 0 }}>Система квестов</h2>
<button
style={styles.closeButton}
onClick={onClose}
>
</button>
</div>
<div style={styles.tabs}>
<button
style={activeTab === 'available' ? { ...styles.tab, ...styles.activeTab } : styles.tab}
onClick={() => setActiveTab('available')}
>
Доступные
</button>
<button
style={activeTab === 'in_progress' ? { ...styles.tab, ...styles.activeTab } : styles.tab}
onClick={() => setActiveTab('in_progress')}
>
В процессе
</button>
<button
style={activeTab === 'completed' ? { ...styles.tab, ...styles.activeTab } : styles.tab}
onClick={() => setActiveTab('completed')}
>
Завершенные
</button>
<button
style={activeTab === 'locked' ? { ...styles.tab, ...styles.activeTab } : styles.tab}
onClick={() => setActiveTab('locked')}
>
Заблокированные
</button>
</div>
<div style={styles.questList}>
{loading ? (
<div style={styles.loading}>Загрузка квестов...</div>
) : error ? (
<div style={styles.error}>
<p>Ошибка загрузки квестов: {error}</p>
<button
style={{ ...styles.button, ...styles.viewButton }}
onClick={loadQuests}
>
Попробовать снова
</button>
</div>
) : filteredQuests.length === 0 ? (
<div style={styles.loading}>
{activeTab === 'available' && 'Нет доступных квестов'}
{activeTab === 'in_progress' && 'Нет активных квестов'}
{activeTab === 'completed' && 'Нет завершенных квестов'}
{activeTab === 'locked' && 'Нет заблокированных квестов'}
</div>
) : (
filteredQuests.map(quest => (
<div
key={quest.id}
style={styles.questItem}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(70, 70, 70, 0.7)';
e.currentTarget.style.borderColor = '#666';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = styles.questItem.background;
e.currentTarget.style.borderColor = styles.questItem.borderColor;
}}
onClick={() => setSelectedQuest(selectedQuest?.id === quest.id ? null : quest)}
>
<div style={styles.questHeader}>
<h3 style={styles.questTitle}>{quest.title}</h3>
<span style={getStatusStyle(quest.status)}>
{getStatusText(quest.status)}
</span>
</div>
<p style={styles.questDescription}>{quest.description}</p>
{quest.currentStep && quest.status === 'in_progress' && (
<div style={{ margin: '10px 0', padding: '8px', background: 'rgba(33, 150, 243, 0.1)', borderRadius: '4px' }}>
<strong>Текущий шаг:</strong> {quest.currentStep.title}
</div>
)}
<div style={styles.questActions}>
{quest.status === 'available' && quest.hasAccess && (
<button
style={{ ...styles.button, ...styles.startButton }}
onClick={(e) => {
e.stopPropagation();
startQuest(quest.id);
}}
>
Начать квест
</button>
)}
{(quest.status === 'in_progress' || quest.status === 'completed') && (
<button
style={{ ...styles.button, ...styles.viewButton }}
onClick={(e) => {
e.stopPropagation();
setSelectedQuest(selectedQuest?.id === quest.id ? null : quest);
}}
>
{selectedQuest?.id === quest.id ? 'Скрыть детали' : 'Показать детали'}
</button>
)}
{quest.status === 'locked' && (
<button
style={{ ...styles.button, ...styles.disabledButton }}
disabled
>
Недоступно
</button>
)}
</div>
{selectedQuest?.id === quest.id && (
<div style={styles.questDetail}>
<h4>Шаги квеста:</h4>
<div style={styles.stepList}>
{quest.steps.map(step => {
let stepStyle = styles.stepItem;
if (step.id === quest.currentStep?.id) {
stepStyle = { ...styles.stepItem, ...styles.currentStep };
} else if (step.playerStatus === 'completed') {
stepStyle = { ...styles.stepItem, ...styles.completedStep };
}
return (
<div key={step.id} style={stepStyle}>
<div style={{ fontWeight: 'bold' }}>
Шаг {step.stepIndex + 1}: {step.title}
{step.isOptional && ' (Опциональный)'}
</div>
<div style={{ marginTop: '5px', fontSize: '14px' }}>
{step.description}
</div>
{step.playerStatus && (
<div style={{
fontSize: '12px',
marginTop: '5px',
color: step.playerStatus === 'completed' ? '#4CAF50' : '#2196F3'
}}>
Статус: {step.playerStatus === 'completed' ? 'Завершен' : 'В процессе'}
</div>
)}
</div>
);
})}
</div>
</div>
)}
</div>
))
)}
</div>
</div>
);
};
export default QuestSystem;

545
src/pages/README.md Normal file
View File

@@ -0,0 +1,545 @@
# Страницы EEV_Proj
## Обзор
Страницы React приложения EEV_Proj. Каждая страница представляет собой отдельный маршрут в приложении и отвечает за определенную функциональность.
## Структура
```
pages/
├── DoubleTapWrapper.jsx # Обертка для двойного тапа
├── InteriorEditor.jsx # Редактор интерьеров
├── Login.jsx # Страница входа
├── Login copy.jsx # Копия страницы входа
├── MapEditor.jsx # Редактор карты
├── RegisterStep1.jsx # Регистрация - шаг 1
├── RegisterStep2.jsx # Регистрация - шаг 2
├── RegisterStep3.jsx # Регистрация - шаг 3
├── WaveformPlayer.jsx # Плеер аудио
└── README.md # Эта документация
```
## DoubleTapWrapper.jsx
Обертка для обработки двойного тапа на мобильных устройствах.
**Функциональность:**
- Обработка двойного тапа
- Предотвращение случайных нажатий
- Настройка задержки между тапами
**Использование:**
```javascript
<DoubleTapWrapper onDoubleTap={handleDoubleTap}>
<div>Контент для двойного тапа</div>
</DoubleTapWrapper>
```
## InteriorEditor.jsx
Редактор интерьеров для создания и настройки внутренних пространств зданий.
**Функциональность:**
- Загрузка 3D моделей интерьеров
- Размещение объектов
- Настройка коллайдеров
- Экспорт конфигурации
**Компоненты:**
- 3D сцена для предварительного просмотра
- Панель инструментов
- Список доступных объектов
- Настройки материалов
## Login.jsx
Основная страница входа в систему.
**Функциональность:**
- Форма входа с email/паролем
- Валидация данных
- Обработка ошибок
- Перенаправление после входа
- Ссылка на регистрацию
**Состояния:**
- `email` - Email пользователя
- `password` - Пароль
- `loading` - Состояние загрузки
- `error` - Сообщение об ошибке
## MapEditor.jsx
Редактор карты мира для настройки игрового пространства.
**Функциональность:**
- Создание и редактирование карты
- Размещение объектов города
- Настройка путей и маршрутов
- Экспорт карты
**Инструменты:**
- Кисть для рисования
- Ластик для удаления
- Выбор объектов
- Масштабирование и панорамирование
## RegisterStep1.jsx
Первый шаг регистрации - основная информация.
**Функциональность:**
- Ввод имени пользователя
- Ввод email адреса
- Ввод пароля
- Подтверждение пароля
- Валидация данных
**Валидация:**
- Уникальность имени пользователя
- Корректность email
- Сложность пароля
- Совпадение паролей
## RegisterStep2.jsx
Второй шаг регистрации - профиль персонажа.
**Функциональность:**
- Выбор аватара
- Выбор пола персонажа
- Настройка внешности
- Предварительный просмотр
**Опции:**
- Готовые аватары
- Настройка цветов
- Выбор причесок
- Настройка лица
## RegisterStep3.jsx
Третий шаг регистрации - завершение.
**Функциональность:**
- Подтверждение данных
- Создание аккаунта
- Активация
- Перенаправление в игру
**Процесс:**
- Проверка всех данных
- Отправка на сервер
- Обработка ответа
- Успешная регистрация
## WaveformPlayer.jsx
Аудио плеер с визуализацией волновой формы.
**Функциональность:**
- Воспроизведение аудио
- Визуализация волновой формы
- Управление воспроизведением
- Настройка громкости
**Контролы:**
- Play/Pause
- Стоп
- Регулировка громкости
- Прогресс-бар
- Временные метки
## Создание новых страниц
### Шаблон страницы
```javascript
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
/**
* Описание страницы
* @param {Object} props - Свойства страницы
*/
const NewPage = (props) => {
const navigate = useNavigate();
const { id } = useParams();
// Состояния
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Эффекты
useEffect(() => {
loadData();
}, [id]);
// Функции
const loadData = async () => {
try {
setLoading(true);
// Загрузка данных
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleSubmit = (event) => {
event.preventDefault();
// Обработка отправки
};
// Рендер
if (loading) return <div>Загрузка...</div>;
if (error) return <div>Ошибка: {error}</div>;
return (
<div className="new-page">
<h1>Новая страница</h1>
<form onSubmit={handleSubmit}>
{/* Форма */}
</form>
</div>
);
};
export default NewPage;
```
### Принципы
1. **Единая ответственность** - Каждая страница отвечает за одну функциональность
2. **Маршрутизация** - Используйте React Router для навигации
3. **Состояние** - Управляйте состоянием через хуки React
4. **Обработка ошибок** - Всегда обрабатывайте ошибки и состояния загрузки
5. **Доступность** - Обеспечивайте доступность для всех пользователей
## Маршрутизация
### Настройка маршрутов
```javascript
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Login from './pages/Login';
import RegisterStep1 from './pages/RegisterStep1';
import RegisterStep2 from './pages/RegisterStep2';
import RegisterStep3 from './pages/RegisterStep3';
import Game from './pages/Game';
const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register/step1" element={<RegisterStep1 />} />
<Route path="/register/step2" element={<RegisterStep2 />} />
<Route path="/register/step3" element={<RegisterStep3 />} />
<Route path="/game" element={<Game />} />
<Route path="/" element={<Navigate to="/login" />} />
</Routes>
</BrowserRouter>
);
};
```
### Защищенные маршруты
```javascript
import RequireProfile from '../components/RequireProfile';
const ProtectedRoute = ({ children }) => {
return (
<RequireProfile redirectTo="/login">
{children}
</RequireProfile>
);
};
// В маршрутах
<Route
path="/game"
element={
<ProtectedRoute>
<Game />
</ProtectedRoute>
}
/>
```
## Навигация
### Программная навигация
```javascript
import { useNavigate } from 'react-router-dom';
const LoginPage = () => {
const navigate = useNavigate();
const handleLogin = async (credentials) => {
try {
const response = await loginUser(credentials);
if (response.success) {
navigate('/game');
}
} catch (error) {
console.error('Ошибка входа:', error);
}
};
return (
<div>
<button onClick={() => navigate('/register/step1')}>
Регистрация
</button>
</div>
);
};
```
### Передача параметров
```javascript
// Передача через navigate
navigate('/user/123', { state: { userData } });
// Получение в компоненте
import { useLocation } from 'react-router-dom';
const UserPage = () => {
const location = useLocation();
const userData = location.state?.userData;
return <div>Данные пользователя: {userData?.name}</div>;
};
```
## Состояние страниц
### Локальное состояние
```javascript
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const handleChange = (event) => {
const { name, value } = event.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
```
### Глобальное состояние
```javascript
import { useAuth } from '../contexts/AuthContext';
const ProfilePage = () => {
const { user, updateUser } = useAuth();
const handleUpdate = async (newData) => {
await updateUser(newData);
};
return <div>Профиль: {user?.username}</div>;
};
```
## Валидация
### Формы
```javascript
import { useState } from 'react';
const LoginForm = () => {
const [errors, setErrors] = useState({});
const validateForm = (data) => {
const newErrors = {};
if (!data.email) {
newErrors.email = 'Email обязателен';
} else if (!/\S+@\S+\.\S+/.test(data.email)) {
newErrors.email = 'Email некорректен';
}
if (!data.password) {
newErrors.password = 'Пароль обязателен';
} else if (data.password.length < 6) {
newErrors.password = 'Пароль должен быть не менее 6 символов';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (event) => {
event.preventDefault();
if (validateForm(formData)) {
// Отправка формы
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-text">{errors.email}</span>}
</form>
);
};
```
## Обработка ошибок
### Error Boundary
```javascript
import { ErrorBoundary } from 'react-error-boundary';
const ErrorFallback = ({ error, resetErrorBoundary }) => {
return (
<div role="alert">
<h2>Что-то пошло не так</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Попробовать снова</button>
</div>
);
};
const App = () => {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Router>
<Routes>
{/* Маршруты */}
</Routes>
</Router>
</ErrorBoundary>
);
};
```
### Try-Catch в компонентах
```javascript
const DataPage = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const result = await api.getData();
setData(result);
} catch (err) {
setError(err.message);
console.error('Ошибка загрузки данных:', err);
}
};
fetchData();
}, []);
if (error) {
return (
<div className="error-container">
<h3>Ошибка загрузки</h3>
<p>{error}</p>
<button onClick={() => window.location.reload()}>
Обновить страницу
</button>
</div>
);
}
return <div>{/* Контент */}</div>;
};
```
## Тестирование
### Unit тесты для страниц
```javascript
import { render, screen, fireEvent } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import Login from './Login';
const renderWithRouter = (component) => {
return render(
<BrowserRouter>
{component}
</BrowserRouter>
);
};
describe('Login Page', () => {
it('отображает форму входа', () => {
renderWithRouter(<Login />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/пароль/i)).toBeInTheDocument();
});
it('обрабатывает отправку формы', () => {
const mockOnSubmit = jest.fn();
renderWithRouter(<Login onSubmit={mockOnSubmit} />);
fireEvent.click(screen.getByRole('button', { name: /войти/i }));
expect(mockOnSubmit).toHaveBeenCalled();
});
});
```
### Интеграционные тесты
```javascript
import { render, screen, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from '../contexts/AuthContext';
import Login from './Login';
const renderWithProviders = (component) => {
return render(
<BrowserRouter>
<AuthProvider>
{component}
</AuthProvider>
</BrowserRouter>
);
};
describe('Login Integration', () => {
it('выполняет вход и перенаправляет', async () => {
renderWithProviders(<Login />);
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'test@example.com' }
});
fireEvent.change(screen.getByLabelText(/пароль/i), {
target: { value: 'password123' }
});
fireEvent.click(screen.getByRole('button', { name: /войти/i }));
await waitFor(() => {
expect(window.location.pathname).toBe('/game');
});
});
});
```

70
src/test-collision.js Normal file
View File

@@ -0,0 +1,70 @@
import { GameCore } from './modules/GameCore.js';
/**
* Тестовый класс для демонстрации системы коллизий в интерьерах
*/
class CollisionTest {
constructor() {
this.gameCore = null;
this.container = null;
}
/**
* Инициализация теста
*/
async init() {
// Создаем контейнер для игры
this.container = document.createElement('div');
this.container.style.width = '100vw';
this.container.style.height = '100vh';
this.container.style.position = 'fixed';
this.container.style.top = '0';
this.container.style.left = '0';
document.body.appendChild(this.container);
// Инициализируем игровое ядро
this.gameCore = new GameCore(this.container);
console.log('Тест системы коллизий инициализирован');
console.log('Управление:');
console.log('- WASD или стрелки для движения');
console.log('- Мышь для поворота камеры в интерьере');
console.log('- Клик по объектам города для входа в интерьер');
}
/**
* Запуск теста
*/
start() {
console.log('Тест запущен');
}
/**
* Остановка теста
*/
stop() {
if (this.gameCore) {
this.gameCore.dispose();
}
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
console.log('Тест остановлен');
}
}
// Автоматический запуск теста при загрузке страницы
if (typeof window !== 'undefined') {
window.addEventListener('load', async () => {
const test = new CollisionTest();
await test.init();
test.start();
// Обработка закрытия страницы
window.addEventListener('beforeunload', () => {
test.stop();
});
});
}
export { CollisionTest };

View File

@@ -0,0 +1,131 @@
// Тест расширенного функционала редактора коллизий
// Файл: test-advanced-collision-editor.js
console.log('🚀 Тестирование расширенного функционала редактора коллизий');
console.log('');
console.log('✨ Новые возможности:');
console.log('');
console.log('1. 📐 Управление параметрами коллайдера:');
console.log(' - Позиция (X, Y, Z) - точное позиционирование');
console.log(' - Поворот (X, Y, Z) - в радианах');
console.log(' - Масштаб (X, Y, Z) - изменение размера');
console.log(' - Кнопка "Применить параметры" для обновления');
console.log('');
console.log('2. 🔄 Дублирование коллайдеров:');
console.log(' - Кнопка "Дублировать коллайдер"');
console.log(' - Копирует все параметры (позиция, поворот, масштаб, цвет, прозрачность)');
console.log(' - Создает копию со смещением на 2 единицы');
console.log(' - Автоматически выбирает новый коллайдер');
console.log('');
console.log('3. 🗑️ Удаление коллайдеров:');
console.log(' - Кнопка "Удалить коллайдер"');
console.log(' - Удаляет выбранный коллайдер из сцены');
console.log(' - Очищает TransformControls');
console.log(' - Обновляет список коллайдеров');
console.log('');
console.log('4. 🚀 Телепорт к интерьерам:');
console.log(' - Секция "Телепорт к интерьерам"');
console.log(' - Список всех доступных интерьеров');
console.log(' - Показывает координаты каждого интерьера');
console.log(' - Мгновенная телепортация камеры');
console.log(' - Автоматическое наведение на интерьер');
console.log('');
console.log('🎮 Как использовать:');
console.log('');
console.log('📐 Управление параметрами:');
console.log('1. Выберите коллайдер кликом');
console.log('2. В секции "Параметры трансформации" введите новые значения');
console.log('3. Нажмите "Применить параметры"');
console.log('4. Коллайдер обновится в реальном времени');
console.log('');
console.log('🔄 Дублирование:');
console.log('1. Выберите коллайдер для копирования');
console.log('2. Нажмите "Дублировать коллайдер"');
console.log('3. Новый коллайдер появится рядом с оригиналом');
console.log('4. Новый коллайдер автоматически выберется');
console.log('');
console.log('🗑️ Удаление:');
console.log('1. Выберите коллайдер для удаления');
console.log('2. Нажмите "Удалить коллайдер"');
console.log('3. Коллайдер исчезнет из сцены');
console.log('');
console.log('🚀 Телепорт:');
console.log('1. В секции "Телепорт к интерьерам" выберите интерьер');
console.log('2. Камера мгновенно переместится к интерьеру');
console.log('3. Камера автоматически наведется на интерьер');
console.log('');
console.log('🔧 Технические детали:');
console.log('');
console.log('📐 Параметры трансформации:');
console.log('- Позиция: точные координаты в 3D пространстве');
console.log('- Поворот: углы в радианах (0 = 0°, π/2 = 90°, π = 180°)');
console.log('- Масштаб: множители размера (1.0 = оригинальный размер)');
console.log('- Валидация: минимальный масштаб 0.1');
console.log('- Шаг: 0.1 для точной настройки');
console.log('');
console.log('🔄 Дублирование:');
console.log('- Копирует геометрию (box/circle/capsule)');
console.log('- Копирует материал и цвет');
console.log('- Копирует все трансформации');
console.log('- Смещение: +2 по X и Z для избежания наложения');
console.log('- Автоматическое обновление TransformControls');
console.log('');
console.log('🗑️ Удаление:');
console.log('- Удаление из Three.js сцены');
console.log('- Удаление из массива collidersRef');
console.log('- Отключение TransformControls');
console.log('- Сброс выбранного объекта');
console.log('');
console.log('🚀 Телепорт:');
console.log('- Загрузка интерьеров через API /api/interiors');
console.log('- Позиционирование камеры: (pos_x, pos_y + 10, pos_z)');
console.log('- Наведение OrbitControls на интерьер');
console.log('- Прокручиваемый список интерьеров');
console.log('- Отображение координат для каждого интерьера');
console.log('');
console.log('🎯 Преимущества:');
console.log('');
console.log('✅ Точность:');
console.log('- Числовые поля для точного ввода параметров');
console.log('- Поддержка десятичных значений');
console.log('- Валидация входных данных');
console.log('');
console.log('✅ Удобство:');
console.log('- Автоматическое обновление при выборе коллайдера');
console.log('- Мгновенная обратная связь');
console.log('- Интуитивный интерфейс');
console.log('');
console.log('✅ Производительность:');
console.log('- Эффективное дублирование объектов');
console.log('- Оптимизированное удаление');
console.log('- Быстрая телепортация');
console.log('');
console.log('✅ Функциональность:');
console.log('- Полный контроль над коллайдерами');
console.log('- Быстрое перемещение по сцене');
console.log('- Удобное создание похожих объектов');
console.log('');
console.log('🚀 Готово к тестированию!');
console.log('Откройте: http://localhost:4000/enhanced-collision-editor');

73
test-camera-controls.js Normal file
View File

@@ -0,0 +1,73 @@
// Тест улучшенного управления камерой в редакторе коллизий
// Файл: test-camera-controls.js
console.log('🎮 Тестирование улучшенного управления камерой');
console.log('');
console.log('📋 Доступные функции:');
console.log('1. Управление клавиатурой:');
console.log(' - W / ↑ - Движение вперед');
console.log(' - S / ↓ - Движение назад');
console.log(' - A / ← - Движение влево');
console.log(' - D / → - Движение вправо');
console.log(' - Q / PageUp - Движение вверх');
console.log(' - E / PageDown - Движение вниз');
console.log(' - R - Сброс позиции камеры');
console.log('');
console.log('2. Управление мышью:');
console.log(' - Левая кнопка + движение - Поворот камеры');
console.log(' - Правая кнопка + движение - Панорамирование');
console.log(' - Колесо мыши - Приближение/отдаление');
console.log('');
console.log('3. Настройки в интерфейсе:');
console.log(' - Слайдер скорости движения (1-20)');
console.log(' - Кнопка "Сброс камеры"');
console.log(' - Кнопка "Вид сверху"');
console.log(' - Кнопка "Вид сбоку"');
console.log('');
console.log('4. Улучшенные OrbitControls:');
console.log(' - Плавное движение с демпфированием');
console.log(' - Настраиваемая скорость панорамирования');
console.log(' - Настраиваемая скорость приближения');
console.log(' - Настраиваемая скорость поворота');
console.log(' - Ограничения расстояния (5-500 единиц)');
console.log('');
console.log('🚀 Как протестировать:');
console.log('1. Откройте редактор: http://localhost:4000/enhanced-collision-editor');
console.log('2. Попробуйте управление клавиатурой (WASD + стрелки)');
console.log('3. Используйте Q/E для движения вверх/вниз');
console.log('4. Нажмите R для сброса позиции');
console.log('5. Попробуйте кнопки быстрого перемещения');
console.log('6. Измените скорость движения слайдером');
console.log('');
console.log('✨ Особенности:');
console.log('- Движение камеры синхронизировано с OrbitControls');
console.log('- Поддержка множественных клавиш одновременно');
console.log('- Плавное движение без рывков');
console.log('- Информация о позиции камеры в реальном времени');
console.log('- Совместимость с существующим функционалом редактора');
console.log('');
console.log('🎯 Рекомендации по использованию:');
console.log('- Используйте WASD для точного позиционирования');
console.log('- Стрелки клавиатуры для альтернативного управления');
console.log('- Q/E для вертикального движения');
console.log('- R для быстрого возврата к исходной позиции');
console.log('- Кнопки видов для быстрого переключения ракурсов');
console.log('- Настройте скорость под свои предпочтения');
console.log('');
console.log('🔧 Технические детали:');
console.log('- Используется requestAnimationFrame для плавности');
console.log('- Обработчики keydown/keyup для отзывчивости');
console.log('- Векторная математика для корректного движения');
console.log('- Синхронизация с target OrbitControls');
console.log('- Автоматическая очистка обработчиков событий');
console.log('');
console.log('✅ Готово к тестированию!');

53
test-collider-creation.js Normal file
View File

@@ -0,0 +1,53 @@
// Тест улучшенного создания коллайдеров в редакторе
// Файл: test-collider-creation.js
console.log('🎯 Тестирование улучшенного создания коллайдеров');
console.log('');
console.log('✨ Что было исправлено:');
console.log('1. Коллайдеры теперь создаются ПЕРЕД камерой, а не в начале координат');
console.log('2. Добавлена настройка расстояния создания (5-50 единиц)');
console.log('3. Добавлен предварительный просмотр позиции коллайдера');
console.log('4. Предварительный просмотр обновляется в реальном времени');
console.log('');
console.log('🎮 Как использовать:');
console.log('1. Откройте редактор: http://localhost:4000/enhanced-collision-editor');
console.log('2. Настройте расстояние создания коллайдера слайдером');
console.log('3. Включите "Показать предварительный просмотр"');
console.log('4. Перемещайте камеру - предварительный просмотр следует за ней');
console.log('5. Нажмите "Создать коллайдер" - он появится перед камерой');
console.log('');
console.log('🔧 Технические детали:');
console.log('- Используется camera.getWorldDirection() для определения направления');
console.log('- Позиция вычисляется: camera.position + direction * distance');
console.log('- Y координата устанавливается на уровне земли (0) или выше');
console.log('- Предварительный просмотр отображается как wireframe с половинной прозрачностью');
console.log('- Предварительный просмотр автоматически удаляется после создания коллайдера');
console.log('');
console.log('⚙️ Настройки:');
console.log('- Расстояние: 5-50 единиц (по умолчанию 10)');
console.log('- Предварительный просмотр: включен/выключен');
console.log('- Тип коллайдера: коробка/цилиндр/капсула');
console.log('- Цвет и прозрачность применяются к предварительному просмотру');
console.log('');
console.log('🎯 Преимущества:');
console.log('- Точное позиционирование коллайдеров');
console.log('- Визуальная обратная связь перед созданием');
console.log('- Настраиваемое расстояние для разных ситуаций');
console.log('- Интуитивное управление');
console.log('- Экономия времени на позиционировании');
console.log('');
console.log('🚀 Рекомендации:');
console.log('- Используйте предварительный просмотр для точного размещения');
console.log('- Настройте расстояние в зависимости от размера объектов');
console.log('- Перемещайте камеру для оптимального угла обзора');
console.log('- Используйте разные типы коллайдеров для разных объектов');
console.log('');
console.log('✅ Готово к тестированию!');

View File

@@ -0,0 +1,141 @@
// Тест отладки проблемы с удалением коллайдеров
// Файл: test-collider-deletion-debug.js
console.log('🔍 Отладка проблемы с удалением коллайдеров');
console.log('');
console.log('❓ Проблема:');
console.log('Созданные и дублированные коллайдеры не удаляются, а старые удаляются');
console.log('');
console.log('🔧 Добавленная отладка:');
console.log('');
console.log('1. 📊 Отладочная информация в функции удаления:');
console.log(' - Логирование выбранного коллайдера');
console.log(' - Подсчет коллайдеров до и после удаления');
console.log(' - Проверка успешности удаления');
console.log('');
console.log('2. 📊 Отладочная информация в функциях создания:');
console.log(' - Логирование создания нового коллайдера');
console.log(' - Подсчет общего количества коллайдеров');
console.log(' - Проверка добавления в массив');
console.log('');
console.log('3. 📊 Отладочная информация в функции дублирования:');
console.log(' - Логирование дублирования коллайдера');
console.log(' - Подсчет общего количества коллайдеров');
console.log(' - Проверка добавления в массив');
console.log('');
console.log('4. 🎯 Отладочная информация в функции выбора:');
console.log(' - Логирование выбранного коллайдера');
console.log(' - Подсчет коллайдеров в массиве');
console.log(' - Проверка корректности выбора');
console.log('');
console.log('5. 🔍 Функция отладки коллайдеров:');
console.log(' - Кнопка "🔍 Отладка коллайдеров"');
console.log(' - Показывает все коллайдеры в массиве');
console.log(' - Отображает их свойства и userData');
console.log('');
console.log('🧪 Как тестировать:');
console.log('');
console.log('1. 📦 Создание коллайдера:');
console.log(' - Нажмите "Создать коллайдер"');
console.log(' - Проверьте консоль: должно появиться "✅ Создан коллайдер"');
console.log(' - Проверьте счетчик: "📊 Всего коллайдеров: X"');
console.log('');
console.log('2. 🔄 Дублирование коллайдера:');
console.log(' - Выберите коллайдер');
console.log(' - Нажмите "Дублировать коллайдер"');
console.log(' - Проверьте консоль: должно появиться "✅ Коллайдер дублирован"');
console.log(' - Проверьте счетчик: количество должно увеличиться');
console.log('');
console.log('3. 🎯 Выбор коллайдера:');
console.log(' - Кликните по коллайдеру');
console.log(' - Проверьте консоль: должно появиться "🎯 Выбран коллайдер"');
console.log(' - Проверьте счетчик: "📊 Всего коллайдеров в массиве: X"');
console.log('');
console.log('4. 🗑️ Удаление коллайдера:');
console.log(' - Выберите коллайдер');
console.log(' - Нажмите "Удалить коллайдер"');
console.log(' - Проверьте консоль:');
console.log(' * "🗑️ Удаляем коллайдер: [объект]"');
console.log(' * "📊 Всего коллайдеров до удаления: X"');
console.log(' * "📊 Коллайдеров до: X, после: Y"');
console.log(' * "✅ Коллайдер успешно удален"');
console.log('');
console.log('5. 🔍 Отладка коллайдеров:');
console.log(' - Нажмите кнопку "🔍 Отладка коллайдеров"');
console.log(' - Проверьте консоль:');
console.log(' * "🔍 Отладка коллайдеров:"');
console.log(' * "📊 Всего коллайдеров: X"');
console.log(' * "🎯 Выбранный коллайдер: [объект или null]"');
console.log(' * "📦 Коллайдер 0: {mesh, data, position, userData}"');
console.log('');
console.log('🔍 Возможные причины проблемы:');
console.log('');
console.log('1. 🔗 Проблема с ссылками:');
console.log(' - Коллайдер может быть добавлен в сцену, но не в массив');
console.log(' - Или наоборот: в массив, но не в сцену');
console.log(' - Проверьте логи создания/дублирования');
console.log('');
console.log('2. 🎯 Проблема с выбором:');
console.log(' - Raycaster может не находить новые коллайдеры');
console.log(' - Проверьте логи выбора коллайдера');
console.log('');
console.log('3. 🗑️ Проблема с удалением:');
console.log(' - Фильтрация массива может работать неправильно');
console.log(' - Проверьте логи удаления');
console.log('');
console.log('4. 🔄 Проблема с состоянием:');
console.log(' - React состояние может не обновляться');
console.log(' - Проверьте, обновляется ли selected');
console.log('');
console.log('📋 Чек-лист для диагностики:');
console.log('');
console.log('✅ Создание:');
console.log('- [ ] В консоли появляется "✅ Создан коллайдер"');
console.log('- [ ] Счетчик коллайдеров увеличивается');
console.log('- [ ] Коллайдер виден в сцене');
console.log('- [ ] Коллайдер можно выбрать');
console.log('');
console.log('✅ Дублирование:');
console.log('- [ ] В консоли появляется "✅ Коллайдер дублирован"');
console.log('- [ ] Счетчик коллайдеров увеличивается');
console.log('- [ ] Новый коллайдер виден в сцене');
console.log('- [ ] Новый коллайдер можно выбрать');
console.log('');
console.log('✅ Выбор:');
console.log('- [ ] В консоли появляется "🎯 Выбран коллайдер"');
console.log('- [ ] TransformControls активируется');
console.log('- [ ] Параметры обновляются в UI');
console.log('');
console.log('✅ Удаление:');
console.log('- [ ] В консоли появляется "🗑️ Удаляем коллайдер"');
console.log('- [ ] Счетчик коллайдеров уменьшается');
console.log('- [ ] Коллайдер исчезает из сцены');
console.log('- [ ] TransformControls отключается');
console.log('');
console.log('🚀 Готово к тестированию!');
console.log('Откройте: http://localhost:4000/enhanced-collision-editor');
console.log('Откройте консоль браузера для просмотра логов');

View File

@@ -0,0 +1,174 @@
// Исправление проблемы с удалением коллайдеров
// Файл: test-collider-deletion-fix.js
console.log('🔧 Исправление проблемы с удалением коллайдеров');
console.log('');
console.log('❓ Проблема:');
console.log('Не удаётся удалить старый коллайдер');
console.log('Удаляю, сохраняю, но после перезагрузки он опять появляется');
console.log('');
console.log('🔍 Причина проблемы:');
console.log('');
console.log('1. 🗑️ Неполное удаление:');
console.log(' - Функция deleteSelected удаляла коллайдер только из фронтенда');
console.log(' - Коллайдер оставался в базе данных');
console.log(' - При перезагрузке коллайдер загружался обратно из БД');
console.log('');
console.log('2. 🔄 Проблема с автоматическим сохранением:');
console.log(' - Автоматическое сохранение полагалось на перезапись всех коллайдеров');
console.log(' - Если сохранение не сработало или сработало с ошибкой, коллайдер оставался');
console.log(' - Отсутствие прямого удаления из БД');
console.log('');
console.log('3. 🆔 Отсутствие ID у новых коллайдеров:');
console.log(' - Новые коллайдеры создавались без ID');
console.log(' - Их нельзя было удалить через API');
console.log(' - Только автоматическое сохранение могло их удалить');
console.log('');
console.log('✅ Исправления:');
console.log('');
console.log('1. 🗑️ Прямое удаление из базы данных:');
console.log(' - Функция deleteSelected теперь асинхронная');
console.log(' - Если у коллайдера есть ID, он удаляется из БД через API');
console.log(' - DELETE /api/colliders/:colliderId');
console.log('');
console.log('2. 🔄 Улучшенная обработка ошибок:');
console.log(' - Логирование успешного удаления из БД');
console.log(' - Обработка ошибок при удалении');
console.log(' - Fallback для коллайдеров без ID');
console.log('');
console.log('3. 🔄 Функция синхронизации:');
console.log(' - Новая функция reloadColliders() для принудительной перезагрузки');
console.log(' - Кнопка "🔄 Синхронизировать" в UI');
console.log(' - Возможность проверить состояние БД');
console.log('');
console.log('4. 📊 Улучшенное логирование:');
console.log(' - Подробные логи процесса удаления');
console.log(' - Информация об ID коллайдеров');
console.log(' - Отслеживание количества коллайдеров');
console.log('');
console.log('🔧 Технические детали:');
console.log('');
console.log('🗑️ Логика удаления:');
console.log('- Проверка наличия ID: if (selected.userData.id)');
console.log('- DELETE запрос: /api/colliders/${selected.userData.id}');
console.log('- Удаление из сцены: sceneRef.current.remove(selected)');
console.log('- Удаление из массива: collidersRef.current.filter()');
console.log('- Автоматическое сохранение для синхронизации');
console.log('');
console.log('🔄 API endpoints:');
console.log('- DELETE /api/colliders/:colliderId - удаление конкретного коллайдера');
console.log('- POST /api/colliders/city/:cityId - перезапись всех коллайдеров');
console.log('- GET /api/colliders/city/:cityId - загрузка коллайдеров');
console.log('');
console.log('🆔 ID система:');
console.log('- Старые коллайдеры: имеют ID из базы данных');
console.log('- Новые коллайдеры: id: null до первого сохранения');
console.log('- Дублированные: id: null до первого сохранения');
console.log('- После сохранения: получают ID из БД');
console.log('');
console.log('🧪 Как тестировать исправления:');
console.log('');
console.log('1. 🗑️ Удаление старых коллайдеров:');
console.log(' - Выберите коллайдер, который был создан ранее');
console.log(' - Нажмите "Удалить коллайдер"');
console.log(' - Проверьте консоль: "✅ Коллайдер X удален из базы данных"');
console.log(' - Перезагрузите страницу');
console.log(' - Убедитесь, что коллайдер не появился');
console.log('');
console.log('2. 🆕 Удаление новых коллайдеров:');
console.log(' - Создайте новый коллайдер');
console.log(' - Сохраните его (чтобы получить ID)');
console.log(' - Удалите коллайдер');
console.log(' - Перезагрузите страницу');
console.log(' - Убедитесь, что коллайдер удален');
console.log('');
console.log('3. 🔄 Синхронизация:');
console.log(' - Удалите несколько коллайдеров');
console.log(' - Нажмите "🔄 Синхронизировать"');
console.log(' - Проверьте консоль: "🔄 Принудительная перезагрузка коллайдеров из БД"');
console.log(' - Убедитесь, что удаленные коллайдеры не загрузились');
console.log('');
console.log('4. 📊 Отладочная информация:');
console.log(' - Проверьте консоль на наличие ошибок');
console.log(' - Должны быть логи о процессе удаления');
console.log(' - Проверьте количество коллайдеров до и после удаления');
console.log('');
console.log('5. 🎯 Тест с несколькими коллайдерами:');
console.log(' - Создайте несколько коллайдеров');
console.log(' - Удалите некоторые из них');
console.log(' - Сохраните изменения');
console.log(' - Перезагрузите страницу');
console.log(' - Убедитесь, что только нужные коллайдеры остались');
console.log('');
console.log('🎯 Ожидаемые результаты:');
console.log('');
console.log('✅ Удаление старых коллайдеров:');
console.log('- [ ] Коллайдеры с ID удаляются из базы данных');
console.log('- [ ] После перезагрузки коллайдеры не появляются');
console.log('- [ ] Логирование успешного удаления');
console.log('- [ ] Отсутствие ошибок в консоли');
console.log('');
console.log('✅ Удаление новых коллайдеров:');
console.log('- [ ] Новые коллайдеры удаляются из фронтенда');
console.log('- [ ] Автоматическое сохранение синхронизирует изменения');
console.log('- [ ] После перезагрузки коллайдеры не появляются');
console.log('- [ ] Корректная работа с коллайдерами без ID');
console.log('');
console.log('✅ Синхронизация:');
console.log('- [ ] Кнопка "Синхронизировать" работает');
console.log('- [ ] Принудительная перезагрузка из БД');
console.log('- [ ] Синхронизация фронтенда с базой данных');
console.log('- [ ] Отсутствие рассинхронизации');
console.log('');
console.log('✅ Обработка ошибок:');
console.log('- [ ] Логирование ошибок при удалении');
console.log('- [ ] Graceful fallback для коллайдеров без ID');
console.log('- [ ] Отсутствие критических ошибок');
console.log('- [ ] Стабильная работа системы');
console.log('');
console.log('🔍 Отладочная информация:');
console.log('');
console.log('📊 В консоли должно появляться:');
console.log('- "🗑️ Удаляем коллайдер: [объект]"');
console.log('- "✅ Коллайдер X удален из базы данных"');
console.log('- "📊 Коллайдеров до: X, после: Y"');
console.log('- "✅ Коллайдер успешно удален"');
console.log('- "💾 Автоматическое сохранение выполнено"');
console.log('');
console.log('🎯 Проверка в базе данных:');
console.log('- SELECT * FROM colliders WHERE city_id = 1;');
console.log('- Удаленные коллайдеры не должны присутствовать');
console.log('- Проверить updated_at для оставшихся коллайдеров');
console.log('');
console.log('🚀 Исправления готовы к тестированию!');
console.log('Откройте: http://localhost:4000/enhanced-collision-editor');
console.log('Попробуйте удалить коллайдер и проверить, что он не появляется после перезагрузки');

View File

@@ -0,0 +1,160 @@
// Тест исправления сохранения перемещенных коллайдеров
// Файл: test-collider-position-saving.js
console.log('🔧 Исправление сохранения перемещенных коллайдеров');
console.log('');
console.log('❓ Проблема:');
console.log('Перемещаю старый коллайдер, нажимаю "сохранить", но при новом входе он стоит на том же месте');
console.log('');
console.log('🔍 Причина проблемы:');
console.log('');
console.log('1. 🆔 Отсутствие ID у коллайдеров:');
console.log(' - Старые коллайдеры загружались из JSON без ID');
console.log(' - Новые коллайдеры создавались без ID');
console.log(' - Сервер не мог различить существующие и новые коллайдеры');
console.log('');
console.log('2. 🔄 Неправильная логика сохранения:');
console.log(' - Сервер удалял ВСЕ коллайдеры и создавал заново');
console.log(' - Потеря связи между фронтендом и базой данных');
console.log(' - Новые позиции не сохранялись');
console.log('');
console.log('✅ Исправления:');
console.log('');
console.log('1. 🆔 Добавление ID к коллайдерам:');
console.log(' - Старые коллайдеры получают ID из базы данных при загрузке');
console.log(' - Новые коллайдеры помечаются как id: null');
console.log(' - Дублированные коллайдеры помечаются как id: null');
console.log('');
console.log('2. 🔄 Улучшенная логика сохранения:');
console.log(' - Разделение коллайдеров на существующие (с ID) и новые (без ID)');
console.log(' - UPDATE для существующих коллайдеров');
console.log(' - INSERT для новых коллайдеров');
console.log(' - Сохранение связи между фронтендом и БД');
console.log('');
console.log('3. 🗄️ Обновление серверной логики:');
console.log(' - Фильтрация коллайдеров по наличию ID');
console.log(' - UPDATE запросы для существующих коллайдеров');
console.log(' - INSERT запросы для новых коллайдеров');
console.log(' - Логирование количества обновленных и созданных коллайдеров');
console.log('');
console.log('🔧 Технические детали:');
console.log('');
console.log('🆔 ID система:');
console.log('- Старые коллайдеры: mesh.userData.id = c.id (из БД)');
console.log('- Новые коллайдеры: mesh.userData.id = null');
console.log('- Дублированные: mesh.userData.id = null');
console.log('- При сохранении: id: mesh.userData.id');
console.log('');
console.log('🔄 Логика сохранения:');
console.log('- existingColliders = colliders.filter(c => c.id)');
console.log('- newColliders = colliders.filter(c => !c.id)');
console.log('- UPDATE для существующих с WHERE id = $1');
console.log('- INSERT для новых без ID');
console.log('');
console.log('🗄️ Серверные изменения:');
console.log('- Разделение коллайдеров на две группы');
console.log('- UPDATE запросы для существующих');
console.log('- INSERT запросы для новых');
console.log('- Транзакционная безопасность');
console.log('');
console.log('🧪 Как тестировать исправления:');
console.log('');
console.log('1. 🔄 Перемещение существующих коллайдеров:');
console.log(' - Загрузите редактор коллизий');
console.log(' - Выберите один из старых коллайдеров');
console.log(' - Переместите его в новое место');
console.log(' - Нажмите "Сохранить сейчас"');
console.log(' - Проверьте консоль: должно появиться "💾 Сохраняем: X существующих, Y новых"');
console.log(' - Перезагрузите страницу');
console.log(' - Убедитесь, что коллайдер остался в новом месте');
console.log('');
console.log('2. 🆕 Создание новых коллайдеров:');
console.log(' - Создайте новый коллайдер');
console.log(' - Переместите его в нужное место');
console.log(' - Нажмите "Сохранить сейчас"');
console.log(' - Проверьте консоль: должно появиться сообщение о создании');
console.log(' - Перезагрузите страницу');
console.log(' - Убедитесь, что новый коллайдер сохранился');
console.log('');
console.log('3. 🔄 Дублирование коллайдеров:');
console.log(' - Выберите существующий коллайдер');
console.log(' - Нажмите "Дублировать коллайдер"');
console.log(' - Переместите дублированный коллайдер');
console.log(' - Нажмите "Сохранить сейчас"');
console.log(' - Перезагрузите страницу');
console.log(' - Убедитесь, что оба коллайдера сохранились');
console.log('');
console.log('4. 🎯 Автоматическое сохранение:');
console.log(' - Переместите коллайдер');
console.log(' - Подождите 2 секунды');
console.log(' - Проверьте консоль: "💾 Автоматическое сохранение выполнено"');
console.log(' - Перезагрузите страницу');
console.log(' - Убедитесь, что изменения сохранились');
console.log('');
console.log('🎯 Ожидаемые результаты:');
console.log('');
console.log('✅ Перемещение существующих:');
console.log('- [ ] Коллайдеры имеют ID из базы данных');
console.log('- [ ] При сохранении выполняется UPDATE');
console.log('- [ ] Позиция сохраняется в базе данных');
console.log('- [ ] После перезагрузки коллайдер остается в новом месте');
console.log('');
console.log('✅ Создание новых:');
console.log('- [ ] Новые коллайдеры помечены как id: null');
console.log('- [ ] При сохранении выполняется INSERT');
console.log('- [ ] Новый коллайдер получает ID в базе данных');
console.log('- [ ] После перезагрузки новый коллайдер загружается');
console.log('');
console.log('✅ Дублирование:');
console.log('- [ ] Дублированные коллайдеры помечены как id: null');
console.log('- [ ] При сохранении выполняется INSERT');
console.log('- [ ] Оригинал обновляется, копия создается');
console.log('- [ ] После перезагрузки оба коллайдера сохраняются');
console.log('');
console.log('✅ Автоматическое сохранение:');
console.log('- [ ] Срабатывает через 2 секунды после изменений');
console.log('- [ ] Правильно обрабатывает существующие и новые коллайдеры');
console.log('- [ ] Логирует количество обновленных и созданных');
console.log('- [ ] Сохраняет все изменения в базе данных');
console.log('');
console.log('🔍 Отладочная информация:');
console.log('');
console.log('📊 В консоли должно появляться:');
console.log('- "💾 Сохраняем: X существующих, Y новых коллайдеров"');
console.log('- "✅ Коллайдеры для города 1 сохранены в БД (X обновлено, Y новых)"');
console.log('- "💾 Автоматическое сохранение выполнено"');
console.log('');
console.log('🎯 Проверка в базе данных:');
console.log('- SELECT * FROM colliders WHERE city_id = 1;');
console.log('- Проверить updated_at для перемещенных коллайдеров');
console.log('- Проверить created_at для новых коллайдеров');
console.log('');
console.log('🚀 Исправления готовы к тестированию!');
console.log('Откройте: http://localhost:4000/enhanced-collision-editor');
console.log('Попробуйте переместить старый коллайдер и сохранить изменения');

97
test-colliders.js Normal file
View File

@@ -0,0 +1,97 @@
// Тестовый скрипт для проверки размеров коллайдеров
// Запустите этот код в консоли браузера для тестирования
console.log('🧪 Тестирование системы коллайдеров');
// Проверяем текущую конфигурацию
console.log('Текущая конфигурация:', window.colliderConfig);
// Тестируем разные коэффициенты
const testMultipliers = [1.0, 2.0, 3.0, 4.0, 5.0];
console.log('📊 Тестируем разные коэффициенты:');
testMultipliers.forEach(multiplier => {
console.log(`\n--- Тест с коэффициентом ${multiplier} ---`);
window.updateColliderSize(multiplier);
// Ждем немного для обновления
setTimeout(() => {
console.log('Размеры коллайдеров обновлены');
window.testCollisions();
}, 100);
});
// Функция для быстрого тестирования
window.quickTest = () => {
console.log('🚀 Быстрый тест коллайдеров и объектов интерьера');
// Включаем режим отладки
window.toggleColliderDebug();
// Тестируем цвета коллайдеров
setTimeout(() => {
console.log('Тестируем синий цвет коллайдеров...');
window.setColliderColor(0, 0, 1); // Синий
}, 1000);
setTimeout(() => {
console.log('Тестируем зеленый цвет коллайдеров...');
window.setColliderColor(0, 1, 0); // Зеленый
}, 2000);
// Тестируем цвета объектов интерьера
setTimeout(() => {
console.log('Тестируем красный цвет объектов интерьера...');
window.setInteriorObjectColor(1, 0, 0); // Красный
}, 3000);
setTimeout(() => {
console.log('Тестируем желтый цвет объектов интерьера...');
window.setInteriorObjectColor(1, 1, 0); // Желтый
}, 4000);
// Тестируем прозрачность
setTimeout(() => {
console.log('Тестируем полупрозрачность объектов интерьера...');
window.setInteriorObjectOpacity(0.5);
}, 5000);
setTimeout(() => {
console.log('Тестируем полную прозрачность объектов интерьера...');
window.setInteriorObjectOpacity(0.1);
}, 6000);
// Тестируем случайные цвета коллайдеров
setTimeout(() => {
console.log('Тестируем случайные цвета коллайдеров...');
window.randomizeColliderColors();
}, 7000);
// Возвращаем стандартные настройки
setTimeout(() => {
console.log('Возвращаем стандартные настройки...');
window.setColliderColor(1, 0, 0); // Красный коллайдер
window.setInteriorObjectColor(1, 1, 1); // Белый объект
window.setInteriorObjectOpacity(1.0); // Полная непрозрачность
}, 8000);
};
console.log('✅ Тестовые функции загружены!');
console.log('Используйте window.quickTest() для быстрого тестирования');
console.log('Используйте window.updateColliderSize(коэффициент) для изменения размера');
console.log('Используйте window.toggleColliderDebug() для включения/выключения визуализации');
console.log('');
console.log('🔧 Функции диагностики:');
console.log('window.debugInteriorObjects() - диагностика объектов интерьера');
console.log('window.setInteriorObjectColor(r,g,b) - цвет объектов интерьера');
console.log('window.setAllObjectsColor(r,g,b) - цвет ВСЕХ объектов в сцене');
console.log('');
console.log('🎯 Целевые функции (только объекты из JSON):');
console.log('window.setColliderObjectsColor(r,g,b) - цвет только объектов из JSON коллайдеров');
console.log('window.applyJsonColorsToObjects() - применить цвета и прозрачность из JSON');
console.log('');
console.log('🎨 Примеры использования:');
console.log('window.applyJsonColorsToObjects() - применить настройки из JSON');
console.log('window.setColliderObjectsColor(1, 0, 0) - красный цвет объектов из JSON');
console.log('window.setColliderObjectsColor(0, 1, 0) - зеленый цвет объектов из JSON');
console.log('window.setAllObjectsColor(0, 0, 1) - синий цвет всех объектов (для сравнения)');

View File

@@ -0,0 +1,145 @@
// Тест исправления ошибок редактора коллизий
// Файл: test-collision-editor-error-fixes.js
console.log('🔧 Исправление ошибок редактора коллизий');
console.log('');
console.log('❓ Проблемы из лога:');
console.log('');
console.log('1. 🚨 Ошибка 500 (Internal Server Error):');
console.log(' ПРОБЛЕМА: Сервер возвращал ошибку 500 при загрузке/сохранении');
console.log(' ПРИЧИНА: Таблица colliders не была создана в базе данных');
console.log(' РЕШЕНИЕ: Выполнена миграция для создания таблицы');
console.log('');
console.log('2. 📄 JSON файл не найден:');
console.log(' ПРОБЛЕМА: Фронтенд не мог загрузить коллайдеры');
console.log(' ПРИЧИНА: Новые API endpoints не работали');
console.log(' РЕШЕНИЕ: Добавлен fallback на старые JSON endpoints');
console.log('');
console.log('3. 👻 Не видны старые коллайдеры:');
console.log(' ПРОБЛЕМА: Коллайдеры из интерьера не отображались');
console.log(' ПРИЧИНА: Данные были только в JSON, не в БД');
console.log(' РЕШЕНИЕ: Мигрированы данные из JSON в базу данных');
console.log('');
console.log('✅ Выполненные исправления:');
console.log('');
console.log('1. 🗄️ Миграция базы данных:');
console.log(' - Создана таблица colliders с полной структурой');
console.log(' - Добавлены индексы для производительности');
console.log(' - Созданы триггеры для автоматического обновления');
console.log(' - Настроены внешние ключи для целостности данных');
console.log('');
console.log('2. 🔄 Fallback система:');
console.log(' - При ошибке 500 автоматически переключается на старый JSON API');
console.log(' - Работает как для загрузки, так и для сохранения');
console.log(' - Логирование переключения для отладки');
console.log(' - Обратная совместимость с существующими данными');
console.log('');
console.log('3. 📊 Миграция данных:');
console.log(' - Перенесены все 6 коллайдеров из JSON в БД');
console.log(' - Сохранены все параметры: позиция, поворот, масштаб, цвет, прозрачность');
console.log(' - Транзакционная безопасность миграции');
console.log(' - Проверка целостности данных');
console.log('');
console.log('🔧 Технические детали:');
console.log('');
console.log('🗄️ Структура таблицы colliders:');
console.log('- id: SERIAL PRIMARY KEY');
console.log('- city_id: INTEGER (связь с городами)');
console.log('- type: VARCHAR(20) (box, circle, capsule)');
console.log('- position_x/y/z: DECIMAL(15,6) (координаты)');
console.log('- rotation_x/y/z: DECIMAL(15,6) (углы в радианах)');
console.log('- scale_x/y/z: DECIMAL(15,6) (масштаб)');
console.log('- color_r/g/b: DECIMAL(3,2) (RGB 0-1)');
console.log('- opacity: DECIMAL(3,2) (прозрачность 0-1)');
console.log('- created_at/updated_at: TIMESTAMP');
console.log('');
console.log('🔄 Fallback логика:');
console.log('- Сначала пробует новый API: /api/colliders/city/:cityId');
console.log('- При ошибке 500 переключается на старый: /api/colliders?cityId=:cityId');
console.log('- Логирует переключение: "🔄 Новый API недоступен, пробуем старый JSON API..."');
console.log('- Сохраняет обратную совместимость');
console.log('');
console.log('📊 Мигрированные данные:');
console.log('- 6 коллайдеров типа "box"');
console.log('- Различные позиции в интерьере');
console.log('- Цвета: зеленые (0.2, 1, 0.1) и красные (1, 0, 0)');
console.log('- Все параметры трансформации сохранены');
console.log('');
console.log('🧪 Как тестировать исправления:');
console.log('');
console.log('1. 🔄 Загрузка коллайдеров:');
console.log(' - Откройте редактор коллизий');
console.log(' - Проверьте консоль: должно появиться "✅ Загружено 6 коллайдеров"');
console.log(' - Убедитесь, что коллайдеры видны в 3D сцене');
console.log(' - Проверьте, что они имеют правильные цвета');
console.log('');
console.log('2. 💾 Сохранение коллайдеров:');
console.log(' - Создайте новый коллайдер');
console.log(' - Подождите 2 секунды для автоматического сохранения');
console.log(' - Проверьте консоль: "💾 Автоматическое сохранение выполнено"');
console.log(' - Перезагрузите страницу и убедитесь, что коллайдер сохранился');
console.log('');
console.log('3. 🎯 Выбор коллайдеров:');
console.log(' - Кликните по коллайдеру');
console.log(' - Проверьте консоль: "🎯 Выбран коллайдер"');
console.log(' - Убедитесь, что TransformControls активируется');
console.log(' - Проверьте обновление параметров в UI');
console.log('');
console.log('4. 🔄 Fallback система:');
console.log(' - Временно отключите БД (если возможно)');
console.log(' - Попробуйте загрузить/сохранить коллайдеры');
console.log(' - Проверьте консоль: должно появиться сообщение о переключении');
console.log(' - Убедитесь, что система работает через JSON');
console.log('');
console.log('🎯 Ожидаемые результаты:');
console.log('');
console.log('✅ Загрузка:');
console.log('- [ ] Нет ошибок 500 в консоли');
console.log('- [ ] Загружается 6 коллайдеров из БД');
console.log('- [ ] Коллайдеры видны в 3D сцене');
console.log('- [ ] Правильные цвета и позиции');
console.log('');
console.log('✅ Сохранение:');
console.log('- [ ] Автоматическое сохранение работает');
console.log('- [ ] Ручное сохранение работает');
console.log('- [ ] Данные сохраняются в БД');
console.log('- [ ] Нет ошибок при сохранении');
console.log('');
console.log('✅ Управление:');
console.log('- [ ] Выбор коллайдеров работает');
console.log('- [ ] TransformControls активируется');
console.log('- [ ] Параметры обновляются в UI');
console.log('- [ ] Дублирование и удаление работают');
console.log('');
console.log('✅ Fallback:');
console.log('- [ ] При ошибке БД переключается на JSON');
console.log('- [ ] Логируется переключение');
console.log('- [ ] Система остается работоспособной');
console.log('- [ ] Обратная совместимость сохранена');
console.log('');
console.log('🚀 Все исправления готовы к тестированию!');
console.log('Откройте: http://localhost:4000/enhanced-collision-editor');
console.log('Проверьте, что все коллайдеры загружаются и работают корректно');

View File

@@ -0,0 +1,129 @@
// Тест исправлений редактора коллизий
// Файл: test-collision-editor-fixes.js
console.log('🔧 Тестирование исправлений редактора коллизий');
console.log('');
console.log('✅ Исправленные проблемы:');
console.log('');
console.log('1. 🔄 Дублирование коллайдера:');
console.log(' ПРОБЛЕМА: У нового коллайдера была видна только рамка, внутри пустота');
console.log(' РЕШЕНИЕ: Исправлено создание материала при дублировании');
console.log(' - Создается новый MeshBasicMaterial с правильными параметрами');
console.log(' - Копируется цвет и прозрачность из оригинального коллайдера');
console.log(' - Создается новый LineBasicMaterial для рамки');
console.log(' - Материал правильно применяется к геометрии');
console.log('');
console.log('2. 📐 Применение параметров трансформации:');
console.log(' ПРОБЛЕМА: При нажатии "Применить параметры" ничего не происходило');
console.log(' РЕШЕНИЕ: Добавлено обновление TransformControls');
console.log(' - Вызывается transformRef.current.updateMatrixWorld()');
console.log(' - TransformControls корректно отображает изменения');
console.log(' - Параметры применяются в реальном времени');
console.log('');
console.log('3. 🎛️ Переключение режимов TransformControls:');
console.log(' НОВАЯ ФУНКЦИЯ: Добавлено переключение между осями');
console.log(' - Кнопки "Перемещение", "Поворот", "Масштаб"');
console.log(' - Визуальная индикация активного режима');
console.log(' - Функция switchTransformMode() для управления');
console.log(' - Автоматическое обновление TransformControls');
console.log('');
console.log('4. 📏 Высота создания коллайдера:');
console.log(' ПРОБЛЕМА: Коллайдер создавался на высоте 0, а не на высоте камеры');
console.log(' РЕШЕНИЕ: Используется высота камеры для создания');
console.log(' - position.y = camera.position.y - 1');
console.log(' - Коллайдер создается на уровне камеры или немного ниже');
console.log(' - Работает корректно в отрицательных координатах');
console.log('');
console.log('🎮 Как использовать исправления:');
console.log('');
console.log('🔄 Дублирование:');
console.log('1. Выберите коллайдер');
console.log('2. Нажмите "Дублировать коллайдер"');
console.log('3. Новый коллайдер появится с правильным материалом');
console.log('4. Цвет и прозрачность будут скопированы');
console.log('');
console.log('📐 Применение параметров:');
console.log('1. Выберите коллайдер');
console.log('2. Измените значения в полях позиции/поворота/масштаба');
console.log('3. Нажмите "Применить параметры"');
console.log('4. Коллайдер обновится и TransformControls покажет изменения');
console.log('');
console.log('🎛️ Режимы трансформации:');
console.log('1. Выберите коллайдер');
console.log('2. Нажмите одну из кнопок: "Перемещение", "Поворот", "Масштаб"');
console.log('3. Активная кнопка подсветится зеленым');
console.log('4. TransformControls переключится в соответствующий режим');
console.log('5. Теперь можно перетаскивать оси для трансформации');
console.log('');
console.log('📏 Создание на высоте камеры:');
console.log('1. Переместите камеру в нужное место');
console.log('2. Нажмите "Создать коллайдер"');
console.log('3. Коллайдер появится перед камерой на её высоте');
console.log('4. Работает даже в отрицательных координатах Y');
console.log('');
console.log('🔧 Технические детали исправлений:');
console.log('');
console.log('🔄 Дублирование материала:');
console.log('- Создается новый MeshBasicMaterial вместо клонирования');
console.log('- Правильно копируется цвет: selected.material.color.clone()');
console.log('- Правильно копируется прозрачность: selected.material.opacity');
console.log('- Создается новый LineBasicMaterial для рамки');
console.log('- Материал применяется к новой геометрии');
console.log('');
console.log('📐 Обновление TransformControls:');
console.log('- Добавлен вызов updateMatrixWorld() после изменения параметров');
console.log('- TransformControls корректно отображает новые значения');
console.log('- Оси обновляются в реальном времени');
console.log('- Поддерживается все три режима трансформации');
console.log('');
console.log('🎛️ Переключение режимов:');
console.log('- Состояние transformMode для отслеживания текущего режима');
console.log('- Функция switchTransformMode() для переключения');
console.log('- Вызов transformRef.current.setMode(mode)');
console.log('- Визуальная индикация активного режима');
console.log('');
console.log('📏 Высота создания:');
console.log('- Используется camera.position.y вместо принудительного 0');
console.log('- position.y = camera.position.y - 1 для небольшого смещения');
console.log('- Работает в любых координатах (положительных и отрицательных)');
console.log('- Коллайдер создается на уровне камеры');
console.log('');
console.log('🎯 Преимущества исправлений:');
console.log('');
console.log('✅ Надежность:');
console.log('- Дублирование работает корректно');
console.log('- Параметры применяются без ошибок');
console.log('- Высота создания предсказуема');
console.log('');
console.log('✅ Удобство:');
console.log('- Визуальное переключение режимов');
console.log('- Интуитивные кнопки управления');
console.log('- Мгновенная обратная связь');
console.log('');
console.log('✅ Функциональность:');
console.log('- Полный контроль над трансформацией');
console.log('- Корректное дублирование объектов');
console.log('- Работа в любых координатах');
console.log('');
console.log('🚀 Все исправления готовы к тестированию!');
console.log('Откройте: http://localhost:4000/enhanced-collision-editor');

405
test-collision.html Normal file
View File

@@ -0,0 +1,405 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Тест системы коллизий интерьеров</title>
<style>
body {
margin: 0;
padding: 0;
background: #000;
color: #fff;
font-family: Arial, sans-serif;
overflow: hidden;
}
#gameContainer {
width: 100vw;
height: 100vh;
position: relative;
}
#debugInfo {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
font-size: 12px;
z-index: 1000;
}
#instructions {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
font-size: 12px;
z-index: 1000;
max-width: 300px;
}
</style>
</head>
<body>
<div id="gameContainer"></div>
<div id="debugInfo">
<div>Статус: <span id="status">Загрузка...</span></div>
<div>Коллайдеры: <span id="colliders">0</span></div>
<div>Позиция игрока: <span id="position">0, 0, 0</span></div>
<div>В интерьере: <span id="inInterior">false</span></div>
</div>
<div id="instructions">
<h3>Управление:</h3>
<p><strong>WASD</strong> - движение</p>
<p><strong>Мышь</strong> - поворот камеры (в интерьере)</p>
<p><strong>Клик по объекту</strong> - вход в интерьер</p>
<p><strong>Escape</strong> - выход из интерьера</p>
<br>
<p><strong>Тест коллизий:</strong></p>
<p>В интерьере игрок не должен проходить сквозь стены и объекты</p>
</div>
<script type="module">
import * as THREE from 'https://unpkg.com/three@0.158.0/build/three.module.js';
// Простая система коллизий для тестирования
class SimpleCollisionTest {
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.player = null;
this.isInInterior = false;
this.interiorColliders = [];
this.moveInput = { forward: false, backward: false, left: false, right: false };
this.init();
}
init() {
// Настройка рендерера
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setClearColor(0x87CEEB);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('gameContainer').appendChild(this.renderer.domElement);
// Создаем простую сцену
this.createScene();
// Создаем игрока
this.createPlayer();
// Настраиваем обработчики событий
this.setupEventListeners();
// Запускаем игровой цикл
this.animate();
this.updateDebugInfo();
}
createScene() {
// Освещение
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 10, 5);
directionalLight.castShadow = true;
this.scene.add(directionalLight);
// Создаем простой интерьер для тестирования
this.createTestInterior();
}
createTestInterior() {
const interiorGroup = new THREE.Group();
// Стены
const wallGeometry = new THREE.BoxGeometry(0.2, 3, 10);
const wallMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 });
// Левая стена
const leftWall = new THREE.Mesh(wallGeometry, wallMaterial);
leftWall.position.set(-5, 1.5, 0);
leftWall.castShadow = true;
interiorGroup.add(leftWall);
// Правая стена
const rightWall = new THREE.Mesh(wallGeometry, wallMaterial);
rightWall.position.set(5, 1.5, 0);
rightWall.castShadow = true;
interiorGroup.add(rightWall);
// Задняя стена
const backWallGeometry = new THREE.BoxGeometry(10, 3, 0.2);
const backWall = new THREE.Mesh(backWallGeometry, wallMaterial);
backWall.position.set(0, 1.5, -5);
backWall.castShadow = true;
interiorGroup.add(backWall);
// Передняя стена (с проходом)
const frontWall1 = new THREE.Mesh(new THREE.BoxGeometry(4, 3, 0.2), wallMaterial);
frontWall1.position.set(-3, 1.5, 5);
frontWall1.castShadow = true;
interiorGroup.add(frontWall1);
const frontWall2 = new THREE.Mesh(new THREE.BoxGeometry(4, 3, 0.2), wallMaterial);
frontWall2.position.set(3, 1.5, 5);
frontWall2.castShadow = true;
interiorGroup.add(frontWall2);
// Пол
const floorGeometry = new THREE.BoxGeometry(10, 0.1, 10);
const floorMaterial = new THREE.MeshLambertMaterial({ color: 0x654321 });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.position.set(0, 0, 0);
floor.receiveShadow = true;
interiorGroup.add(floor);
// Потолок
const ceiling = new THREE.Mesh(floorGeometry, new THREE.MeshLambertMaterial({ color: 0xFFFFFF }));
ceiling.position.set(0, 3, 0);
interiorGroup.add(ceiling);
// Объекты в интерьере
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const boxMaterial = new THREE.MeshLambertMaterial({ color: 0xFF0000 });
const box1 = new THREE.Mesh(boxGeometry, boxMaterial);
box1.position.set(-2, 0.5, -2);
box1.castShadow = true;
interiorGroup.add(box1);
const box2 = new THREE.Mesh(boxGeometry, boxMaterial);
box2.position.set(2, 0.5, 2);
box2.castShadow = true;
interiorGroup.add(box2);
// Собираем коллайдеры
this.interiorColliders = [];
interiorGroup.traverse((child) => {
if (child.isMesh && child.geometry) {
this.interiorColliders.push(child);
}
});
this.scene.add(interiorGroup);
this.interiorGroup = interiorGroup;
console.log('Создано коллайдеров:', this.interiorColliders.length);
}
createPlayer() {
const playerGeometry = new THREE.BoxGeometry(0.6, 1.6, 0.6);
const playerMaterial = new THREE.MeshLambertMaterial({ color: 0x0000FF });
this.player = new THREE.Mesh(playerGeometry, playerMaterial);
this.player.position.set(0, 0.8, 0);
this.scene.add(this.player);
// Устанавливаем камеру на уровне глаз игрока
this.camera.position.set(0, 1.6, 0);
this.camera.lookAt(0, 1.6, -1);
}
setupEventListeners() {
// Клавиатура
document.addEventListener('keydown', (event) => {
switch(event.code) {
case 'KeyW':
this.moveInput.forward = true;
break;
case 'KeyS':
this.moveInput.backward = true;
break;
case 'KeyA':
this.moveInput.left = true;
break;
case 'KeyD':
this.moveInput.right = true;
break;
case 'Escape':
this.exitInterior();
break;
}
});
document.addEventListener('keyup', (event) => {
switch(event.code) {
case 'KeyW':
this.moveInput.forward = false;
break;
case 'KeyS':
this.moveInput.backward = false;
break;
case 'KeyA':
this.moveInput.left = false;
break;
case 'KeyD':
this.moveInput.right = false;
break;
}
});
// Мышь для поворота камеры в интерьере
document.addEventListener('mousemove', (event) => {
if (this.isInInterior) {
this.camera.rotation.y -= event.movementX * 0.002;
this.camera.rotation.x -= event.movementY * 0.002;
this.camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, this.camera.rotation.x));
}
});
// Клик для входа в интерьер
this.renderer.domElement.addEventListener('click', () => {
if (!this.isInInterior) {
this.enterInterior();
}
});
// Изменение размера окна
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
}
enterInterior() {
console.log('Вход в интерьер');
this.isInInterior = true;
this.player.visible = false;
// Запрашиваем pointer lock
this.renderer.domElement.requestPointerLock();
this.updateDebugInfo();
}
exitInterior() {
console.log('Выход из интерьера');
this.isInInterior = false;
this.player.visible = true;
// Выходим из pointer lock
document.exitPointerLock();
this.updateDebugInfo();
}
checkCollision(testPosition) {
const playerRadius = 0.3;
const playerHeight = 1.6;
// Создаем AABB для игрока
const playerBox = new THREE.Box3();
const playerMin = new THREE.Vector3(
testPosition.x - playerRadius,
testPosition.y,
testPosition.z - playerRadius
);
const playerMax = new THREE.Vector3(
testPosition.x + playerRadius,
testPosition.y + playerHeight,
testPosition.z + playerRadius
);
playerBox.setFromPoints([playerMin, playerMax]);
// Проверяем столкновения с коллайдерами
for (const collider of this.interiorColliders) {
if (!collider.geometry || !collider.visible) continue;
collider.updateMatrixWorld(true);
const colliderBox = new THREE.Box3();
colliderBox.setFromObject(collider);
if (playerBox.intersectsBox(colliderBox)) {
return true;
}
}
return false;
}
updatePlayer(deltaTime) {
if (!this.isInInterior) return;
const speed = 3.0;
const moveDistance = speed * deltaTime;
// Используем простые направления вместо кватернионов
const forward = new THREE.Vector3(0, 0, -1);
const right = new THREE.Vector3(1, 0, 0);
// Поворачиваем направления в соответствии с поворотом камеры
forward.applyEuler(new THREE.Euler(0, this.camera.rotation.y, 0));
right.applyEuler(new THREE.Euler(0, this.camera.rotation.y, 0));
let moveVector = new THREE.Vector3();
if (this.moveInput.forward) moveVector.add(forward);
if (this.moveInput.backward) moveVector.add(forward.clone().multiplyScalar(-1));
if (this.moveInput.left) moveVector.add(right.clone().multiplyScalar(-1));
if (this.moveInput.right) moveVector.add(right);
if (moveVector.length() > 0) {
moveVector.normalize().multiplyScalar(moveDistance);
// Проверяем коллизии по осям отдельно
let safePosition = this.camera.position.clone();
// Проверяем движение по X
if (Math.abs(moveVector.x) > 0.001) {
const xTestPosition = safePosition.clone();
xTestPosition.x += moveVector.x;
if (!this.checkCollision(xTestPosition)) {
safePosition.x = xTestPosition.x;
}
}
// Проверяем движение по Z
if (Math.abs(moveVector.z) > 0.001) {
const zTestPosition = safePosition.clone();
zTestPosition.z += moveVector.z;
if (!this.checkCollision(zTestPosition)) {
safePosition.z = zTestPosition.z;
}
}
this.camera.position.copy(safePosition);
}
}
updateDebugInfo() {
document.getElementById('status').textContent = this.isInInterior ? 'В интерьере' : 'Вне интерьера';
document.getElementById('colliders').textContent = this.interiorColliders.length;
document.getElementById('position').textContent =
`${this.camera.position.x.toFixed(2)}, ${this.camera.position.y.toFixed(2)}, ${this.camera.position.z.toFixed(2)}`;
document.getElementById('inInterior').textContent = this.isInInterior;
}
animate() {
requestAnimationFrame(() => this.animate());
const deltaTime = 0.016; // Примерно 60 FPS
this.updatePlayer(deltaTime);
this.updateDebugInfo();
this.renderer.render(this.scene, this.camera);
}
}
// Запускаем тест
const test = new SimpleCollisionTest();
</script>
</body>
</html>

View File

@@ -0,0 +1,173 @@
// Тест всех улучшений редактора коллизий
// Файл: test-complete-collision-editor-improvements.js
console.log('🚀 Полные улучшения редактора коллизий');
console.log('');
console.log('✅ Решенные проблемы:');
console.log('');
console.log('1. 🔄 Сохранение параметров трансформации:');
console.log(' ПРОБЛЕМА: При перемещении объекты плохо сохранялись');
console.log(' РЕШЕНИЕ: Исправлено обновление данных при изменении через TransformControls');
console.log(' - Добавлено обновление UI параметров при изменении через TransformControls');
console.log(' - Автоматическое обновление colliderPosition, colliderRotation, colliderScale');
console.log(' - Логирование изменений для отладки');
console.log('');
console.log('2. 🗄️ Миграция с JSON на базу данных:');
console.log(' ПРОБЛЕМА: Данные хранились в JSON файлах');
console.log(' РЕШЕНИЕ: Полная миграция на PostgreSQL');
console.log(' - Создана таблица colliders с полной структурой');
console.log(' - Новые API endpoints для работы с БД');
console.log(' - Транзакции для надежности');
console.log(' - Индексы для производительности');
console.log('');
console.log('3. 🎛️ Улучшенное управление:');
console.log(' ПРОБЛЕМА: Управление было неудобным');
console.log(' РЕШЕНИЕ: Значительно улучшен интерфейс');
console.log(' - Автоматическое сохранение с настраиваемой задержкой');
console.log(' - Индикация последнего сохранения');
console.log(' - Удобные кнопки сохранения/загрузки');
console.log(' - Переключение режимов TransformControls');
console.log('');
console.log('4. 💾 Автоматическое сохранение:');
console.log(' НОВАЯ ФУНКЦИЯ: Автоматическое сохранение изменений');
console.log(' - Задержка 2 секунды после изменений');
console.log(' - Отмена предыдущих таймеров при новых изменениях');
console.log(' - Индикация статуса сохранения');
console.log(' - Возможность отключения');
console.log('');
console.log('🔧 Технические улучшения:');
console.log('');
console.log('📊 База данных:');
console.log('- Таблица colliders с полной структурой');
console.log('- Поля для позиции, поворота, масштаба, цвета, прозрачности');
console.log('- Автоматические timestamps (created_at, updated_at)');
console.log('- Индексы для быстрого поиска');
console.log('- Триггеры для автоматического обновления updated_at');
console.log('- Внешние ключи для связи с городами');
console.log('');
console.log('🌐 API Endpoints:');
console.log('- GET /api/colliders/city/:cityId - получение коллайдеров');
console.log('- POST /api/colliders/city/:cityId - сохранение всех коллайдеров');
console.log('- PUT /api/colliders/:colliderId - обновление отдельного коллайдера');
console.log('- DELETE /api/colliders/:colliderId - удаление отдельного коллайдера');
console.log('- Транзакции для надежности операций');
console.log('- Валидация входных данных');
console.log('');
console.log('🎮 Улучшенный интерфейс:');
console.log('- Автоматическое сохранение с индикацией');
console.log('- Переключение режимов TransformControls (перемещение/поворот/масштаб)');
console.log('- Удобные кнопки управления');
console.log('- Отображение времени последнего сохранения');
console.log('- Состояния загрузки/сохранения');
console.log('');
console.log('🧪 Как тестировать улучшения:');
console.log('');
console.log('1. 🗄️ Миграция базы данных:');
console.log(' - Запустите: node migrate-colliders.js');
console.log(' - Проверьте создание таблицы colliders');
console.log(' - Убедитесь в наличии индексов и триггеров');
console.log('');
console.log('2. 🔄 Автоматическое сохранение:');
console.log(' - Создайте или измените коллайдер');
console.log(' - Подождите 2 секунды');
console.log(' - Проверьте консоль: "💾 Автоматическое сохранение выполнено"');
console.log(' - Проверьте время последнего сохранения в UI');
console.log('');
console.log('3. 🎛️ Режимы TransformControls:');
console.log(' - Выберите коллайдер');
console.log(' - Переключайтесь между режимами: Перемещение, Поворот, Масштаб');
console.log(' - Проверьте изменение осей в 3D сцене');
console.log(' - Убедитесь в автоматическом сохранении изменений');
console.log('');
console.log('4. 📊 Работа с базой данных:');
console.log(' - Создайте несколько коллайдеров');
console.log(' - Перезагрузите страницу');
console.log(' - Убедитесь, что коллайдеры загружаются из БД');
console.log(' - Проверьте сохранение в БД через API');
console.log('');
console.log('5. 🔧 Удобство управления:');
console.log(' - Используйте кнопки "Сохранить сейчас" и "Перезагрузить"');
console.log(' - Отключите/включите автоматическое сохранение');
console.log(' - Проверьте индикацию состояний загрузки/сохранения');
console.log('');
console.log('🎯 Преимущества улучшений:');
console.log('');
console.log('✅ Надежность:');
console.log('- Транзакции обеспечивают целостность данных');
console.log('- Автоматическое сохранение предотвращает потерю данных');
console.log('- Валидация входных данных');
console.log('- Обработка ошибок');
console.log('');
console.log('✅ Производительность:');
console.log('- Индексы для быстрого поиска');
console.log('- Оптимизированные SQL запросы');
console.log('- Дебаунсинг автоматического сохранения');
console.log('- Эффективное управление состоянием');
console.log('');
console.log('✅ Удобство использования:');
console.log('- Интуитивный интерфейс');
console.log('- Автоматическое сохранение');
console.log('- Визуальная обратная связь');
console.log('- Гибкие настройки');
console.log('');
console.log('✅ Масштабируемость:');
console.log('- База данных вместо файлов');
console.log('- API для интеграции');
console.log('- Структурированные данные');
console.log('- Возможность расширения');
console.log('');
console.log('📋 Чек-лист тестирования:');
console.log('');
console.log('✅ Миграция БД:');
console.log('- [ ] Таблица colliders создана');
console.log('- [ ] Индексы созданы');
console.log('- [ ] Триггеры работают');
console.log('- [ ] Внешние ключи настроены');
console.log('');
console.log('✅ API Endpoints:');
console.log('- [ ] GET /api/colliders/city/:cityId работает');
console.log('- [ ] POST /api/colliders/city/:cityId работает');
console.log('- [ ] PUT /api/colliders/:colliderId работает');
console.log('- [ ] DELETE /api/colliders/:colliderId работает');
console.log('');
console.log('✅ Автоматическое сохранение:');
console.log('- [ ] Сохранение через 2 секунды после изменений');
console.log('- [ ] Отмена предыдущих таймеров');
console.log('- [ ] Индикация времени последнего сохранения');
console.log('- [ ] Возможность отключения');
console.log('');
console.log('✅ Управление:');
console.log('- [ ] Переключение режимов TransformControls');
console.log('- [ ] Автоматическое обновление UI параметров');
console.log('- [ ] Удобные кнопки управления');
console.log('- [ ] Состояния загрузки/сохранения');
console.log('');
console.log('🚀 Все улучшения готовы к тестированию!');
console.log('Откройте: http://localhost:4000/enhanced-collision-editor');
console.log('Сначала выполните миграцию: node migrate-colliders.js');

View File

@@ -0,0 +1,135 @@
// Тест исправления дублирования параметров трансформации
// Файл: test-duplicate-transform-fix.js
console.log('🔧 Исправление дублирования параметров трансформации');
console.log('');
console.log('❓ Проблема:');
console.log('При дублировании коллайдера параметры трансформации не передавались новому объекту');
console.log('');
console.log('✅ Исправление:');
console.log('');
console.log('1. 📐 Копирование параметров трансформации:');
console.log(' - Позиция: mesh.position.copy(selected.position)');
console.log(' - Поворот: mesh.rotation.copy(selected.rotation)');
console.log(' - Масштаб: mesh.scale.copy(selected.scale)');
console.log(' - Смещение: +2 по X и Z для избежания наложения');
console.log('');
console.log('2. 🎛️ Обновление UI параметров:');
console.log(' - setColliderPosition() с новыми координатами');
console.log(' - setColliderRotation() с новыми углами');
console.log(' - setColliderScale() с новыми размерами');
console.log(' - UI автоматически отображает параметры нового коллайдера');
console.log('');
console.log('3. 📊 Улучшенная отладка:');
console.log(' - Логирование скопированных параметров трансформации');
console.log(' - Отображение позиции, поворота и масштаба');
console.log(' - Проверка корректности копирования');
console.log('');
console.log('🎮 Как тестировать исправление:');
console.log('');
console.log('1. 📦 Создайте коллайдер:');
console.log(' - Нажмите "Создать коллайдер"');
console.log(' - Коллайдер появится перед камерой');
console.log('');
console.log('2. 🔧 Измените параметры:');
console.log(' - Выберите коллайдер');
console.log(' - Измените позицию, поворот или масштаб');
console.log(' - Нажмите "Применить параметры"');
console.log(' - Коллайдер обновится');
console.log('');
console.log('3. 🔄 Дублируйте коллайдер:');
console.log(' - Убедитесь, что коллайдер выбран');
console.log(' - Нажмите "Дублировать коллайдер"');
console.log(' - Проверьте консоль:');
console.log(' * "✅ Коллайдер дублирован"');
console.log(' * "📐 Параметры трансформации скопированы:"');
console.log(' * Позиция, поворот, масштаб нового коллайдера');
console.log('');
console.log('4. ✅ Проверьте результат:');
console.log(' - Новый коллайдер должен иметь те же параметры');
console.log(' - Плюс смещение на 2 единицы по X и Z');
console.log(' - UI должен показать параметры нового коллайдера');
console.log(' - TransformControls должен быть прикреплен к новому коллайдеру');
console.log('');
console.log('🔍 Технические детали:');
console.log('');
console.log('📐 Копирование трансформации:');
console.log('- mesh.position.copy(selected.position) - копирует позицию');
console.log('- mesh.position.add(new THREE.Vector3(2, 0, 2)) - добавляет смещение');
console.log('- mesh.rotation.copy(selected.rotation) - копирует поворот');
console.log('- mesh.scale.copy(selected.scale) - копирует масштаб');
console.log('');
console.log('🎛️ Обновление UI:');
console.log('- setColliderPosition() - обновляет поля позиции в UI');
console.log('- setColliderRotation() - обновляет поля поворота в UI');
console.log('- setColliderScale() - обновляет поля масштаба в UI');
console.log('- UI автоматически синхронизируется с новым коллайдером');
console.log('');
console.log('📊 Отладочная информация:');
console.log('- Логирование всех скопированных параметров');
console.log('- Отображение точных значений позиции, поворота, масштаба');
console.log('- Проверка корректности копирования');
console.log('');
console.log('🎯 Преимущества исправления:');
console.log('');
console.log('✅ Полнота копирования:');
console.log('- Все параметры трансформации копируются');
console.log('- Новый коллайдер идентичен оригиналу');
console.log('- Только позиция смещается для избежания наложения');
console.log('');
console.log('✅ Удобство использования:');
console.log('- UI показывает параметры нового коллайдера');
console.log('- Можно сразу редактировать параметры');
console.log('- TransformControls готов к работе');
console.log('');
console.log('✅ Предсказуемость:');
console.log('- Поведение дублирования предсказуемо');
console.log('- Все параметры сохраняются');
console.log('- Смещение всегда одинаковое');
console.log('');
console.log('🧪 Тестовые сценарии:');
console.log('');
console.log('1. 🔄 Дублирование с поворотом:');
console.log(' - Создайте коллайдер');
console.log(' - Поверните его (например, на 45°)');
console.log(' - Дублируйте');
console.log(' - Новый коллайдер должен быть повернут на тот же угол');
console.log('');
console.log('2. 🔄 Дублирование с масштабом:');
console.log(' - Создайте коллайдер');
console.log(' - Увеличьте масштаб (например, в 2 раза)');
console.log(' - Дублируйте');
console.log(' - Новый коллайдер должен иметь тот же масштаб');
console.log('');
console.log('3. 🔄 Дублирование с позицией:');
console.log(' - Создайте коллайдер');
console.log(' - Переместите его в другое место');
console.log(' - Дублируйте');
console.log(' - Новый коллайдер должен быть рядом (смещение +2, +2)');
console.log('');
console.log('🚀 Исправление готово к тестированию!');
console.log('Откройте: http://localhost:4000/enhanced-collision-editor');
console.log('Проверьте дублирование коллайдеров с различными параметрами');

View File

@@ -0,0 +1,167 @@
// Исправление отображения коллизионных объектов в игре
// Файл: test-game-collider-visibility-fix.js
console.log('🔧 Исправление отображения коллизионных объектов в игре');
console.log('');
console.log('❓ Проблема:');
console.log('Коллизионные объекты до сих пор видны, не смотря на то, что их непрозрачность равна нулю');
console.log('');
console.log('🔍 Причина проблемы:');
console.log('');
console.log('1. 🎮 Разная логика в редакторе и игре:');
console.log(' - В редакторе: коллайдеры используют прозрачность из базы данных');
console.log(' - В игре: коллайдеры создаются с фиксированной прозрачностью 0.001');
console.log(' - Игра не учитывает настройки прозрачности из БД');
console.log('');
console.log('2. 🔧 Фиксированная прозрачность в игре:');
console.log(' - const material = new THREE.MeshBasicMaterial({ opacity: 0.001 })');
console.log(' - Все коллизионные объекты имеют одинаковую прозрачность');
console.log(' - Не учитывается значение opacity из базы данных');
console.log('');
console.log('3. 👁️ Отсутствие логики невидимости:');
console.log(' - В игре нет проверки opacity === 0');
console.log(' - Нет установки material.visible = false');
console.log(' - Нет использования alphaTest для правильной прозрачности');
console.log('');
console.log('✅ Исправления:');
console.log('');
console.log('1. 🎮 Использование прозрачности из базы данных:');
console.log(' - const opacity = c.opacity !== undefined ? c.opacity : 0.001');
console.log(' - Каждый коллайдер использует свою прозрачность из БД');
console.log(' - Fallback на 0.001 для старых коллайдеров');
console.log('');
console.log('2. 👁️ Логика невидимости:');
console.log(' - if (opacity === 0) { material.visible = false; alphaTest = 0; }');
console.log(' - else { material.visible = true; alphaTest = 0.1; }');
console.log(' - Правильная обработка полностью прозрачных объектов');
console.log('');
console.log('3. 🔧 Дополнительные функции для отладки:');
console.log(' - window.updateColliderOpacity(opacity) - обновить прозрачность всех коллайдеров');
console.log(' - window.toggleColliderVisibility(visible) - включить/выключить видимость');
console.log(' - window.reloadAllColliders() - перезагрузить все коллайдеры');
console.log('');
console.log('🔧 Технические детали:');
console.log('');
console.log('🎮 Логика создания коллайдеров в игре:');
console.log('- Использование прозрачности из базы данных: c.opacity');
console.log('- Fallback на 0.001 для совместимости');
console.log('- Проверка opacity === 0 для невидимости');
console.log('- Установка material.visible и alphaTest');
console.log('');
console.log('👁️ Обработка прозрачности:');
console.log('- opacity = 0: material.visible = false, alphaTest = 0');
console.log('- opacity > 0: material.visible = true, alphaTest = 0.1');
console.log('- Применяется к каждому коллайдеру индивидуально');
console.log('');
console.log('🔧 Функции отладки:');
console.log('- updateColliderOpacity(opacity): обновить прозрачность всех коллайдеров');
console.log('- toggleColliderVisibility(visible): включить/выключить видимость');
console.log('- reloadAllColliders(): перезагрузить все коллайдеры из БД');
console.log('- checkCollidersInDB(): проверить состояние БД');
console.log('');
console.log('🧪 Как тестировать исправления:');
console.log('');
console.log('1. 🎮 Тест прозрачности в игре:');
console.log(' - Откройте игру: http://localhost:4000');
console.log(' - Проверьте консоль: window.checkCollidersInDB()');
console.log(' - Убедитесь, что коллайдеры загружены из БД');
console.log(' - Коллайдеры с opacity = 0 должны быть невидимы');
console.log(' - Коллайдеры с opacity > 0 должны быть видимы');
console.log('');
console.log('2. 🔧 Тест функций отладки:');
console.log(' - window.updateColliderOpacity(0) - сделать все невидимыми');
console.log(' - window.updateColliderOpacity(0.5) - сделать полупрозрачными');
console.log(' - window.toggleColliderVisibility(false) - выключить видимость');
console.log(' - window.toggleColliderVisibility(true) - включить видимость');
console.log('');
console.log('3. 🔄 Тест перезагрузки:');
console.log(' - window.reloadAllColliders() - перезагрузить все коллайдеры');
console.log(' - Проверьте, что прозрачность применяется корректно');
console.log(' - Убедитесь, что коллайдеры с opacity = 0 невидимы');
console.log('');
console.log('4. 📊 Тест синхронизации с редактором:');
console.log(' - Откройте редактор коллизий');
console.log(' - Установите прозрачность коллайдера в 0');
console.log(' - Сохраните изменения');
console.log(' - Вернитесь в игру');
console.log(' - Выполните window.reloadAllColliders()');
console.log(' - Коллайдер должен стать невидимым');
console.log('');
console.log('5. 🎯 Тест с разными значениями прозрачности:');
console.log(' - Создайте коллайдеры с разной прозрачностью (0, 0.3, 0.7, 1)');
console.log(' - Сохраните изменения');
console.log(' - Перезагрузите игру');
console.log(' - Проверьте, что каждый коллайдер имеет правильную прозрачность');
console.log('');
console.log('🎯 Ожидаемые результаты:');
console.log('');
console.log('✅ Прозрачность в игре:');
console.log('- [ ] Коллайдеры с opacity = 0 полностью невидимы');
console.log('- [ ] Коллайдеры с opacity > 0 видимы с правильной прозрачностью');
console.log('- [ ] Каждый коллайдер использует свою прозрачность из БД');
console.log('- [ ] Fallback на 0.001 для старых коллайдеров');
console.log('');
console.log('✅ Функции отладки:');
console.log('- [ ] window.updateColliderOpacity() работает корректно');
console.log('- [ ] window.toggleColliderVisibility() работает корректно');
console.log('- [ ] window.reloadAllColliders() обновляет прозрачность');
console.log('- [ ] window.checkCollidersInDB() показывает правильные данные');
console.log('');
console.log('✅ Синхронизация с редактором:');
console.log('- [ ] Изменения прозрачности в редакторе отражаются в игре');
console.log('- [ ] Коллайдеры с opacity = 0 невидимы в обоих местах');
console.log('- [ ] Отсутствие рассинхронизации');
console.log('- [ ] Корректная работа автоматического сохранения');
console.log('');
console.log('✅ Производительность:');
console.log('- [ ] Быстрая загрузка коллайдеров с правильной прозрачностью');
console.log('- [ ] Отсутствие задержек при перезагрузке');
console.log('- [ ] Стабильная работа коллизий');
console.log('- [ ] Корректная работа прозрачности');
console.log('');
console.log('🔍 Отладочная информация:');
console.log('');
console.log('📊 В консоли должно появляться:');
console.log('- "📊 Коллайдеры в БД: X штук"');
console.log('- "👁️ Обновляем прозрачность всех коллизионных объектов: Y"');
console.log('- "✅ Прозрачность обновлена для Z коллизионных объектов"');
console.log('- "👁️ Переключаем видимость коллизионных объектов: true/false"');
console.log('- "✅ Видимость коллизионных объектов: включена/выключена"');
console.log('');
console.log('🎯 Проверка в игре:');
console.log('- Коллайдеры с opacity = 0 не должны быть видны');
console.log('- Коллайдеры с opacity > 0 должны быть видны с правильной прозрачностью');
console.log('- Функции отладки должны работать корректно');
console.log('- Перезагрузка должна обновлять прозрачность');
console.log('');
console.log('🚀 Исправления готовы к тестированию!');
console.log('Откройте игру: http://localhost:4000');
console.log('Протестируйте прозрачность коллизионных объектов и функции отладки');

View File

@@ -0,0 +1,153 @@
// Исправление загрузки коллайдеров в игре
// Файл: test-game-colliders-loading.js
console.log('🔧 Исправление загрузки коллайдеров в игре');
console.log('');
console.log('❓ Проблема:');
console.log('Редактор коллизий работает с базой данных, но сама игра загружает коллайдеры из JSON файла');
console.log('Коллайдеры, созданные в редакторе, не отображаются в игре');
console.log('');
console.log('🔍 Причина проблемы:');
console.log('');
console.log('1. 🔄 Разные источники данных:');
console.log(' - Редактор коллизий: загружает из /api/colliders/city/:cityId (база данных)');
console.log(' - Игра: загружает из /colliders_city_1.json (JSON файл)');
console.log(' - Данные не синхронизируются между источниками');
console.log('');
console.log('2. 📊 Две функции загрузки в Game.js:');
console.log(' - loadCollidersFromJSON(): для коллизионных данных (Box3)');
console.log(' - loadCustomCollidersForCity(): для визуальных коллайдеров (Mesh)');
console.log(' - Обе использовали только JSON файлы');
console.log('');
console.log('3. 🗄️ Отсутствие fallback механизма:');
console.log(' - Игра не могла загрузить данные из базы данных');
console.log(' - Нет резервного варианта при недоступности БД');
console.log('');
console.log('✅ Исправления:');
console.log('');
console.log('1. 🔄 Обновление loadCollidersFromJSON():');
console.log(' - Сначала пробует загрузить из /api/colliders/city/:cityId');
console.log(' - При 500 ошибке fallback на /colliders_city_1.json');
console.log(' - Обрабатывает данные из обоих источников');
console.log('');
console.log('2. 🔄 Обновление loadCustomCollidersForCity():');
console.log(' - Сначала пробует загрузить из /api/colliders/city/:cityId');
console.log(' - При 500 ошибке fallback на /api/colliders?cityId=:cityId');
console.log(' - Создает визуальные меши для коллайдеров');
console.log('');
console.log('3. 📊 Улучшенная обработка данных:');
console.log(' - Проверка формата данных (data.colliders vs Array)');
console.log(' - Логирование источника данных');
console.log(' - Отладочная информация для диагностики');
console.log('');
console.log('🔧 Технические детали:');
console.log('');
console.log('🔄 Логика загрузки:');
console.log('- Первая попытка: /api/colliders/city/:cityId (база данных)');
console.log('- Fallback: JSON файлы или старый API');
console.log('- Проверка ответа: response.ok && response.status !== 500');
console.log('- Обработка данных: data.colliders || data (массив)');
console.log('');
console.log('📊 Два типа коллайдеров в игре:');
console.log('- Коллизионные (Box3): для проверки столкновений');
console.log('- Визуальные (Mesh): для отображения в сцене');
console.log('- Оба типа теперь загружаются из базы данных');
console.log('');
console.log('🗄️ API endpoints:');
console.log('- Новый: GET /api/colliders/city/:cityId (база данных)');
console.log('- Старый: GET /api/colliders?cityId=:cityId (JSON файлы)');
console.log('- Fallback: GET /colliders_city_1.json (прямые файлы)');
console.log('');
console.log('🧪 Как тестировать исправления:');
console.log('');
console.log('1. 🎮 Запуск игры:');
console.log(' - Откройте игру: http://localhost:4000');
console.log(' - Проверьте консоль браузера');
console.log(' - Должно появиться: "📊 Загружены коллайдеры из базы данных"');
console.log(' - Или: "📄 Загружены коллайдеры из JSON файла" (fallback)');
console.log('');
console.log('2. 🔍 Проверка коллайдеров:');
console.log(' - Коллайдеры должны быть видны в игре');
console.log(' - Коллизии должны работать корректно');
console.log(' - Проверьте, что персонаж не проходит через коллайдеры');
console.log('');
console.log('3. 🔄 Синхронизация с редактором:');
console.log(' - Создайте коллайдер в редакторе коллизий');
console.log(' - Сохраните изменения');
console.log(' - Перезагрузите игру');
console.log(' - Новый коллайдер должен появиться в игре');
console.log('');
console.log('4. 🗄️ Тест fallback:');
console.log(' - Остановите сервер базы данных');
console.log(' - Перезагрузите игру');
console.log(' - Должно появиться: "🔄 Новый API недоступен, пробуем старый JSON API"');
console.log(' - Игра должна загрузиться с данными из JSON файлов');
console.log('');
console.log('5. 📊 Отладочная информация:');
console.log(' - Проверьте консоль на наличие ошибок');
console.log(' - Должны быть логи о загрузке коллайдеров');
console.log(' - Проверьте количество загруженных коллайдеров');
console.log('');
console.log('🎯 Ожидаемые результаты:');
console.log('');
console.log('✅ Загрузка из базы данных:');
console.log('- [ ] Игра загружает коллайдеры из БД');
console.log('- [ ] Коллайдеры из редактора видны в игре');
console.log('- [ ] Коллизии работают корректно');
console.log('- [ ] Синхронизация между редактором и игрой');
console.log('');
console.log('✅ Fallback механизм:');
console.log('- [ ] При недоступности БД загружаются JSON файлы');
console.log('- [ ] Игра работает в любом случае');
console.log('- [ ] Логирование источника данных');
console.log('- [ ] Отсутствие критических ошибок');
console.log('');
console.log('✅ Производительность:');
console.log('- [ ] Быстрая загрузка коллайдеров');
console.log('- [ ] Отсутствие задержек в игре');
console.log('- [ ] Корректная работа коллизий');
console.log('- [ ] Стабильная работа при переключении городов');
console.log('');
console.log('🔍 Отладочная информация:');
console.log('');
console.log('📊 В консоли должно появляться:');
console.log('- "🔍 loadCollidersFromJSON вызвана для города: 1"');
console.log('- "📊 Загружены коллайдеры из базы данных: X объектов"');
console.log('- "🔍 loadCustomCollidersForCity для города: 1"');
console.log('- "📊 Загружены кастомные коллайдеры из базы данных: X объектов"');
console.log('');
console.log('🎯 Проверка в игре:');
console.log('- Коллайдеры должны быть видны (если включена визуализация)');
console.log('- Персонаж не должен проходить через коллайдеры');
console.log('- Коллизии должны работать плавно');
console.log('');
console.log('🚀 Исправления готовы к тестированию!');
console.log('Откройте игру: http://localhost:4000');
console.log('Проверьте консоль и убедитесь, что коллайдеры загружаются из базы данных');

297
test-interior-api.html Normal file
View File

@@ -0,0 +1,297 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Тест API интерьера</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f0f0f0;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.test-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.result {
background: #f9f9f9;
padding: 10px;
margin: 10px 0;
border-radius: 5px;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
}
.error {
background: #ffe6e6;
color: #d00;
}
.success {
background: #e6ffe6;
color: #060;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
input {
padding: 8px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
</style>
</head>
<body>
<div class="container">
<h1>Тест API интерьера</h1>
<div class="test-section">
<h3>1. Проверка токена</h3>
<button onclick="checkToken()">Проверить токен</button>
<div id="tokenResult" class="result"></div>
</div>
<div class="test-section">
<h3>2. Тест API интерьера</h3>
<input type="number" id="interiorId" placeholder="ID интерьера" value="101">
<button onclick="testInteriorAPI()">Тестировать API</button>
<div id="apiResult" class="result"></div>
</div>
<div class="test-section">
<h3>3. Тест загрузки GLB</h3>
<input type="text" id="glbUrl" placeholder="URL GLB файла">
<button onclick="testGLBLoad()">Тестировать GLB</button>
<div id="glbResult" class="result"></div>
</div>
<div class="test-section">
<h3>4. Полный тест интерьера</h3>
<button onclick="fullInteriorTest()">Полный тест</button>
<div id="fullResult" class="result"></div>
</div>
</div>
<script>
function log(elementId, message, isError = false) {
const element = document.getElementById(elementId);
const timestamp = new Date().toLocaleTimeString();
const className = isError ? 'error' : 'success';
element.innerHTML += `<div class="${className}">[${timestamp}] ${message}</div>`;
}
function clear(elementId) {
document.getElementById(elementId).innerHTML = '';
}
async function checkToken() {
clear('tokenResult');
log('tokenResult', 'Проверяем токен...');
const token = localStorage.getItem('token');
if (!token) {
log('tokenResult', 'Токен не найден! Попробуйте войти в основную игру сначала.', true);
log('tokenResult', 'Или введите токен вручную:', false);
const manualToken = prompt('Введите токен из localStorage основной игры:');
if (manualToken) {
localStorage.setItem('token', manualToken);
log('tokenResult', 'Токен сохранен, повторяем проверку...');
setTimeout(checkToken, 1000);
}
return;
}
log('tokenResult', `Токен найден: ${token.substring(0, 20)}...`);
// Проверяем валидность токена
try {
const response = await fetch('/api/auth/me', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include'
});
if (response.ok) {
const userData = await response.json();
log('tokenResult', `Токен валиден. Пользователь: ${userData.firstName} ${userData.lastName}`);
} else {
log('tokenResult', `Токен невалиден: ${response.status}`, true);
}
} catch (error) {
log('tokenResult', `Ошибка проверки токена: ${error.message}`, true);
}
}
async function testInteriorAPI() {
clear('apiResult');
const interiorId = document.getElementById('interiorId').value;
log('apiResult', `Тестируем API для интерьера ${interiorId}...`);
const token = localStorage.getItem('token');
if (!token) {
log('apiResult', 'Токен не найден!', true);
return;
}
try {
log('apiResult', 'Запрашиваем определение интерьера...');
const response = await fetch(`/api/interiors/${interiorId}/definition`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
cache: 'no-cache'
});
log('apiResult', `Ответ сервера: ${response.status} ${response.statusText}`);
if (!response.ok) {
const errorText = await response.text();
log('apiResult', `Ошибка: ${errorText}`, true);
return;
}
const data = await response.json();
log('apiResult', `Данные получены: ${JSON.stringify(data, null, 2)}`);
if (data.glb) {
const glbUrl = window.location.origin + data.glb;
log('apiResult', `GLB URL: ${glbUrl}`);
// Проверяем доступность GLB файла
try {
const headResponse = await fetch(glbUrl, { method: 'HEAD', cache: 'no-cache' });
log('apiResult', `GLB файл доступен: ${headResponse.status}`);
} catch (error) {
log('apiResult', `GLB файл недоступен: ${error.message}`, true);
}
}
} catch (error) {
log('apiResult', `Ошибка API: ${error.message}`, true);
}
}
async function testGLBLoad() {
clear('glbResult');
const glbUrl = document.getElementById('glbUrl').value;
if (!glbUrl) {
log('glbResult', 'URL GLB не указан!', true);
return;
}
log('glbResult', `Тестируем загрузку GLB: ${glbUrl}`);
try {
// Проверяем доступность файла
const headResponse = await fetch(glbUrl, { method: 'HEAD', cache: 'no-cache' });
log('glbResult', `Файл доступен: ${headResponse.status}`);
if (headResponse.ok) {
// Пробуем загрузить как ArrayBuffer
const response = await fetch(glbUrl, { cache: 'no-cache' });
const arrayBuffer = await response.arrayBuffer();
log('glbResult', `GLB загружен: ${arrayBuffer.byteLength} байт`);
}
} catch (error) {
log('glbResult', `Ошибка загрузки GLB: ${error.message}`, true);
}
}
async function fullInteriorTest() {
clear('fullResult');
const interiorId = document.getElementById('interiorId').value;
log('fullResult', `Полный тест интерьера ${interiorId}...`);
// 1. Проверяем токен
const token = localStorage.getItem('token');
if (!token) {
log('fullResult', 'Токен не найден!', true);
return;
}
// 2. Получаем определение интерьера
try {
log('fullResult', 'Шаг 1: Получаем определение интерьера...');
const defResponse = await fetch(`/api/interiors/${interiorId}/definition`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
cache: 'no-cache'
});
if (!defResponse.ok) {
log('fullResult', `Ошибка получения определения: ${defResponse.status}`, true);
return;
}
const { glb, objects } = await defResponse.json();
log('fullResult', `Определение получено. GLB: ${glb}, Объектов: ${objects ? objects.length : 0}`);
// 3. Проверяем GLB файл
const glbUrl = window.location.origin + glb;
log('fullResult', `Шаг 2: Проверяем GLB файл: ${glbUrl}`);
const headResponse = await fetch(glbUrl, { method: 'HEAD', cache: 'no-cache' });
if (!headResponse.ok) {
log('fullResult', `GLB файл недоступен: ${headResponse.status}`, true);
return;
}
log('fullResult', `GLB файл доступен: ${headResponse.status}`);
// 4. Пробуем загрузить GLB
log('fullResult', 'Шаг 3: Загружаем GLB файл...');
const glbResponse = await fetch(glbUrl, { cache: 'no-cache' });
const arrayBuffer = await glbResponse.arrayBuffer();
log('fullResult', `GLB загружен: ${arrayBuffer.byteLength} байт`);
// 5. Проверяем объекты интерьера
if (objects && objects.length > 0) {
log('fullResult', `Шаг 4: Проверяем объекты интерьера (${objects.length} шт.)...`);
for (let i = 0; i < Math.min(3, objects.length); i++) {
const obj = objects[i];
if (obj.model_url) {
const objUrl = window.location.origin + obj.model_url;
try {
const objResponse = await fetch(objUrl, { method: 'HEAD', cache: 'no-cache' });
log('fullResult', `Объект ${i + 1}: ${objResponse.status} - ${obj.model_url}`);
} catch (error) {
log('fullResult', `Объект ${i + 1}: Ошибка - ${obj.model_url}`, true);
}
}
}
}
log('fullResult', 'Тест завершен успешно!');
} catch (error) {
log('fullResult', `Ошибка теста: ${error.message}`, true);
}
}
// Автоматически проверяем токен при загрузке
window.addEventListener('load', () => {
checkToken();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,176 @@
// Исправление проблем с прозрачностью и коллизиями
// Файл: test-opacity-collision-fixes.js
console.log('🔧 Исправление проблем с прозрачностью и коллизиями');
console.log('');
console.log('❓ Проблемы:');
console.log('1) Даже когда у коллайдера прозрачность 0, всё равно его видно');
console.log('2) В старом json был объект, сейчас я удалил, его не видно, но при этом сквозь него я всё равно пройти не могу');
console.log('');
console.log('🔍 Причины проблем:');
console.log('');
console.log('1. 👁️ Проблема с прозрачностью:');
console.log(' - В Three.js opacity = 0 не делает объект полностью невидимым');
console.log(' - Нужно дополнительно установить visible = false');
console.log(' - Или использовать alphaTest для правильной обработки прозрачности');
console.log('');
console.log('2. 🚫 Проблема с коллизиями удаленных коллайдеров:');
console.log(' - Игра загружает коллайдеры из разных источников');
console.log(' - Коллизионные коллайдеры (Box3) и визуальные (Mesh) загружаются отдельно');
console.log(' - При удалении коллайдера из редактора игра может не перезагрузить коллайдеры');
console.log(' - Кэширование или рассинхронизация между источниками');
console.log('');
console.log('✅ Исправления:');
console.log('');
console.log('1. 👁️ Исправление прозрачности:');
console.log(' - Добавлена проверка opacity === 0');
console.log(' - При opacity = 0: material.visible = false, alphaTest = 0');
console.log(' - При opacity > 0: material.visible = true, alphaTest = 0.1');
console.log(' - Обновлены все функции создания и изменения коллайдеров');
console.log('');
console.log('2. 🚫 Исправление коллизий удаленных коллайдеров:');
console.log(' - Добавлена функция window.reloadAllColliders() в игре');
console.log(' - Добавлена функция window.checkCollidersInDB() для диагностики');
console.log(' - Перезагрузка как коллизионных, так и визуальных коллайдеров');
console.log(' - Сравнение состояния БД с состоянием игры');
console.log('');
console.log('3. 🔄 Улучшенная синхронизация:');
console.log(' - Функция reloadColliders в редакторе');
console.log(' - Кнопка "🔄 Синхронизировать" в UI редактора');
console.log(' - Глобальные функции для отладки в игре');
console.log('');
console.log('🔧 Технические детали:');
console.log('');
console.log('👁️ Логика прозрачности:');
console.log('- if (opacity === 0) { material.visible = false; alphaTest = 0; }');
console.log('- else { material.visible = true; alphaTest = 0.1; }');
console.log('- Применяется к colliderMaterial, загруженным коллайдерам, изменениям в UI');
console.log('');
console.log('🚫 Логика перезагрузки коллайдеров:');
console.log('- loadCollidersFromJSON(1) - коллизионные коллайдеры (Box3)');
console.log('- loadCustomCollidersForCity(1) - визуальные коллайдеры (Mesh)');
console.log('- window.reloadAllColliders() - перезагрузка всех типов');
console.log('- window.checkCollidersInDB() - диагностика состояния');
console.log('');
console.log('🔄 Источники коллайдеров в игре:');
console.log('- Коллизионные: jsonCollidersRef.current (Box3 объекты)');
console.log('- Визуальные: visualCollidersRef.current (Mesh объекты)');
console.log('- Оба загружаются из /api/colliders/city/1 с fallback на JSON');
console.log('');
console.log('🧪 Как тестировать исправления:');
console.log('');
console.log('1. 👁️ Тест прозрачности:');
console.log(' - Откройте редактор коллизий');
console.log(' - Выберите коллайдер');
console.log(' - Установите прозрачность в 0');
console.log(' - Нажмите "Применить цвет"');
console.log(' - Коллайдер должен стать полностью невидимым');
console.log(' - Установите прозрачность обратно в 0.3');
console.log(' - Коллайдер должен стать видимым');
console.log('');
console.log('2. 🚫 Тест коллизий удаленных коллайдеров:');
console.log(' - Откройте игру');
console.log(' - Проверьте консоль: window.checkCollidersInDB()');
console.log(' - Убедитесь, что коллайдеры загружены из БД');
console.log(' - Откройте редактор коллизий');
console.log(' - Удалите коллайдер');
console.log(' - Вернитесь в игру');
console.log(' - Выполните: window.reloadAllColliders()');
console.log(' - Проверьте, что коллизии исчезли');
console.log('');
console.log('3. 🔄 Тест синхронизации:');
console.log(' - В редакторе: нажмите "🔄 Синхронизировать"');
console.log(' - В игре: выполните window.reloadAllColliders()');
console.log(' - Проверьте консоль на наличие ошибок');
console.log(' - Убедитесь, что количество коллайдеров совпадает');
console.log('');
console.log('4. 📊 Диагностика:');
console.log(' - В игре: window.checkCollidersInDB()');
console.log(' - Проверьте количество коллайдеров в БД vs в игре');
console.log(' - Убедитесь, что данные синхронизированы');
console.log('');
console.log('5. 🎯 Тест с несколькими коллайдерами:');
console.log(' - Создайте несколько коллайдеров с разной прозрачностью');
console.log(' - Удалите некоторые из них');
console.log(' - Сохраните изменения');
console.log(' - Перезагрузите игру');
console.log(' - Выполните window.reloadAllColliders()');
console.log(' - Проверьте, что только нужные коллайдеры остались');
console.log('');
console.log('🎯 Ожидаемые результаты:');
console.log('');
console.log('✅ Прозрачность:');
console.log('- [ ] Коллайдеры с opacity = 0 полностью невидимы');
console.log('- [ ] Коллайдеры с opacity > 0 видимы с правильной прозрачностью');
console.log('- [ ] Изменения прозрачности применяются мгновенно');
console.log('- [ ] Сохранение прозрачности работает корректно');
console.log('');
console.log('✅ Коллизии удаленных коллайдеров:');
console.log('- [ ] Удаленные коллайдеры не создают коллизий');
console.log('- [ ] window.reloadAllColliders() обновляет все коллайдеры');
console.log('- [ ] Синхронизация между редактором и игрой');
console.log('- [ ] Отсутствие "призрачных" коллизий');
console.log('');
console.log('✅ Синхронизация:');
console.log('- [ ] Кнопка "Синхронизировать" в редакторе работает');
console.log('- [ ] window.reloadAllColliders() в игре работает');
console.log('- [ ] window.checkCollidersInDB() показывает правильные данные');
console.log('- [ ] Отсутствие рассинхронизации');
console.log('');
console.log('✅ Производительность:');
console.log('- [ ] Быстрая перезагрузка коллайдеров');
console.log('- [ ] Отсутствие задержек в игре');
console.log('- [ ] Стабильная работа коллизий');
console.log('- [ ] Корректная работа прозрачности');
console.log('');
console.log('🔍 Отладочная информация:');
console.log('');
console.log('📊 В консоли должно появляться:');
console.log('- "🔄 Принудительная перезагрузка всех коллайдеров"');
console.log('- "✅ Коллизионные коллайдеры перезагружены"');
console.log('- "✅ Визуальные коллайдеры перезагружены"');
console.log('- "📊 Коллайдеры в БД: X штук"');
console.log('- "📊 Коллизионные коллайдеры в игре: Y штук"');
console.log('');
console.log('🎯 Проверка прозрачности:');
console.log('- Коллайдеры с opacity = 0 не должны быть видны');
console.log('- Коллайдеры с opacity > 0 должны быть видны с правильной прозрачностью');
console.log('- Изменения должны применяться мгновенно');
console.log('');
console.log('🎯 Проверка коллизий:');
console.log('- Удаленные коллайдеры не должны создавать коллизий');
console.log('- Персонаж должен проходить через места, где были удаленные коллайдеры');
console.log('- Коллизии должны работать только для существующих коллайдеров');
console.log('');
console.log('🚀 Исправления готовы к тестированию!');
console.log('Откройте редактор: http://localhost:4000/enhanced-collision-editor');
console.log('Откройте игру: http://localhost:4000');
console.log('Протестируйте прозрачность и коллизии удаленных коллайдеров');

164
test-wireframe-fix.js Normal file
View File

@@ -0,0 +1,164 @@
// Исправление проблемы с рамками коллайдеров
// Файл: test-wireframe-fix.js
console.log('🔧 Исправление проблемы с рамками коллайдеров');
console.log('');
console.log('❓ Проблема:');
console.log('У каждого коллайдера два объекта: один только рамка, другой полностью закрашен');
console.log('При перемещении только рамки не сохраняется перемещение');
console.log('При перемещении полностью закрашенного объекта всё сохраняется');
console.log('');
console.log('🔍 Причина проблемы:');
console.log('');
console.log('1. 🏗️ Структура коллайдеров:');
console.log(' - Каждый коллайдер состоит из основного меша (mesh) и рамки (line)');
console.log(' - Рамка добавляется как дочерний объект: mesh.add(line)');
console.log(' - При клике можно выбрать либо основной меш, либо рамку');
console.log('');
console.log('2. 🎯 Проблема выбора:');
console.log(' - Рамка (LineSegments) не имеет userData с параметрами коллайдера');
console.log(' - Только основной меш имеет правильные userData и обновляется в collidersRef');
console.log(' - При выборе рамки TransformControls прикрепляется к ней, но данные не сохраняются');
console.log('');
console.log('3. 💾 Проблема сохранения:');
console.log(' - updateColliderData работает только с основными мешами');
console.log(' - Рамки не участвуют в процессе сохранения');
console.log(' - Перемещение рамки не влияет на позицию основного меша');
console.log('');
console.log('✅ Исправления:');
console.log('');
console.log('1. 🎯 Улучшенная логика выбора:');
console.log(' - Raycaster теперь проверяет все объекты коллайдеров (меши + рамки)');
console.log(' - При клике по рамке автоматически выбирается родительский меш');
console.log(' - Проверка, что выбранный объект действительно является коллайдером');
console.log('');
console.log('2. 🔲 Опция отключения рамок:');
console.log(' - Новое состояние showWireframes для управления видимостью');
console.log(' - Функция toggleWireframes для переключения видимости');
console.log(' - Кнопка в UI для удобного управления');
console.log('');
console.log('3. 🏗️ Обновление создания коллайдеров:');
console.log(' - Новые коллайдеры учитывают настройку видимости рамок');
console.log(' - Дублированные коллайдеры учитывают настройку видимости рамок');
console.log(' - Загруженные коллайдеры учитывают настройку видимости рамок');
console.log('');
console.log('🔧 Технические детали:');
console.log('');
console.log('🎯 Логика выбора:');
console.log('- allColliderObjects = [основные меши, рамки]');
console.log('- if (clickedObject.type === "LineSegments") clickedObject = clickedObject.parent');
console.log('- Проверка через collidersRef.current.find(c => c.mesh === clickedObject)');
console.log('');
console.log('🔲 Управление рамками:');
console.log('- showWireframes: boolean состояние');
console.log('- toggleWireframes(): переключает видимость всех рамок');
console.log('- line.visible = showWireframes при создании');
console.log('- child.visible = newShowWireframes при переключении');
console.log('');
console.log('🏗️ Структура коллайдера:');
console.log('- mesh (основной меш с материалом)');
console.log('- mesh.children[0] (рамка LineSegments)');
console.log('- mesh.userData (параметры коллайдера)');
console.log('- collidersRef содержит ссылки на основные меши');
console.log('');
console.log('🧪 Как тестировать исправления:');
console.log('');
console.log('1. 🎯 Выбор коллайдеров:');
console.log(' - Загрузите редактор коллизий');
console.log(' - Попробуйте кликнуть по рамке коллайдера');
console.log(' - Проверьте консоль: "🎯 Кликнули по рамке, выбираем родительский меш"');
console.log(' - Убедитесь, что TransformControls прикрепился к основному мешу');
console.log(' - Переместите коллайдер и сохраните');
console.log(' - Перезагрузите страницу и проверьте, что позиция сохранилась');
console.log('');
console.log('2. 🔲 Переключение рамок:');
console.log(' - Нажмите кнопку "🔲 Скрыть рамки"');
console.log(' - Проверьте консоль: "🔲 Рамки отключены"');
console.log(' - Убедитесь, что все рамки исчезли');
console.log(' - Нажмите кнопку "🔲 Показать рамки"');
console.log(' - Проверьте консоль: "🔲 Рамки включены"');
console.log(' - Убедитесь, что все рамки появились');
console.log('');
console.log('3. 🆕 Создание с отключенными рамками:');
console.log(' - Отключите рамки');
console.log(' - Создайте новый коллайдер');
console.log(' - Убедитесь, что новый коллайдер создался без рамки');
console.log(' - Включите рамки');
console.log(' - Убедитесь, что рамка появилась у нового коллайдера');
console.log('');
console.log('4. 🔄 Дублирование с отключенными рамками:');
console.log(' - Отключите рамки');
console.log(' - Дублируйте существующий коллайдер');
console.log(' - Убедитесь, что дублированный коллайдер создался без рамки');
console.log(' - Включите рамки');
console.log(' - Убедитесь, что рамка появилась у дублированного коллайдера');
console.log('');
console.log('5. 💾 Сохранение перемещений:');
console.log(' - Кликните по рамке коллайдера');
console.log(' - Переместите его в новое место');
console.log(' - Нажмите "Сохранить сейчас"');
console.log(' - Перезагрузите страницу');
console.log(' - Убедитесь, что коллайдер остался в новом месте');
console.log('');
console.log('🎯 Ожидаемые результаты:');
console.log('');
console.log('✅ Выбор коллайдеров:');
console.log('- [ ] Клик по рамке выбирает основной меш');
console.log('- [ ] TransformControls прикрепляется к основному мешу');
console.log('- [ ] Перемещение рамки перемещает весь коллайдер');
console.log('- [ ] Сохранение работает корректно');
console.log('');
console.log('✅ Управление рамками:');
console.log('- [ ] Кнопка переключает видимость всех рамок');
console.log('- [ ] Новые коллайдеры учитывают настройку');
console.log('- [ ] Дублированные коллайдеры учитывают настройку');
console.log('- [ ] Загруженные коллайдеры учитывают настройку');
console.log('');
console.log('✅ Сохранение перемещений:');
console.log('- [ ] Перемещение через рамку сохраняется');
console.log('- [ ] Перемещение через основной меш сохраняется');
console.log('- [ ] Автоматическое сохранение работает');
console.log('- [ ] Ручное сохранение работает');
console.log('');
console.log('🔍 Отладочная информация:');
console.log('');
console.log('📊 В консоли должно появляться:');
console.log('- "🎯 Кликнули по рамке, выбираем родительский меш"');
console.log('- "🔲 Рамки включены/отключены"');
console.log('- "🎯 Выбран коллайдер: [объект]"');
console.log('');
console.log('🎯 Проверка структуры:');
console.log('- selected.type должен быть "Mesh" (не "LineSegments")');
console.log('- selected.userData должен содержать параметры коллайдера');
console.log('- selected.children[0].type должен быть "LineSegments"');
console.log('');
console.log('🚀 Исправления готовы к тестированию!');
console.log('Откройте: http://localhost:4000/enhanced-collision-editor');
console.log('Попробуйте кликнуть по рамке коллайдера и переместить его');