Compare commits
13 Commits
32453d6282
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cef990956 | |||
| 1b0bf6f122 | |||
| d11cf0ecb7 | |||
| c189eed962 | |||
| 5d19b6339e | |||
| 261e8a8b63 | |||
| bb58adb1a3 | |||
| e48eadf9c5 | |||
| 70b9d456e1 | |||
| 51995c3695 | |||
| f77d19975e | |||
| 950f29cea6 | |||
| 4f1187915b |
1
.env
1
.env
@@ -1,3 +1,4 @@
|
|||||||
DATABASE_URL=postgres://my_user:scupAs2s@188.120.243.108:5432/game_db
|
DATABASE_URL=postgres://my_user:scupAs2s@188.120.243.108:5432/game_db
|
||||||
JWT_SECRET=tgkkkxd2131
|
JWT_SECRET=tgkkkxd2131
|
||||||
DATABASE_URL_VIRTUAL_WORLD=postgres://my_user:scupAs2s@188.120.243.108:5432/virtual_world
|
DATABASE_URL_VIRTUAL_WORLD=postgres://my_user:scupAs2s@188.120.243.108:5432/virtual_world
|
||||||
|
DATABASE_QUEST_NEW_QUESTS=postgres://my_user:scupAs2s@188.120.243.108:5432/new_quests
|
||||||
149
COLLIDER_CONFIGURATION.md
Normal file
149
COLLIDER_CONFIGURATION.md
Normal 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); // Вернуться к оригинальным размерам
|
||||||
|
```
|
||||||
|
|
||||||
91
COLLISION_DEBUG.md
Normal file
91
COLLISION_DEBUG.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Диагностика сохранения коллизии
|
||||||
|
|
||||||
|
## 🔍 Добавлено подробное логирование для отладки коллизии
|
||||||
|
|
||||||
|
### **В консоли браузера (F12):**
|
||||||
|
|
||||||
|
#### **При изменении свойств объекта:**
|
||||||
|
```
|
||||||
|
🔧 Обновлены свойства объекта: {
|
||||||
|
name: "Название объекта",
|
||||||
|
organization_id: 2,
|
||||||
|
collidable: true, ← Проверьте это значение
|
||||||
|
interior_id: 101
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **При сохранении объекта:**
|
||||||
|
```
|
||||||
|
🔍 Проверяем коллизию: {
|
||||||
|
'obj.userData.collidable': true, ← Значение в объекте
|
||||||
|
'objectData.collidable': true, ← Значение для сервера
|
||||||
|
'objectCollidable state': true ← Значение в состоянии
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **В консоли сервера:**
|
||||||
|
|
||||||
|
#### **При получении данных:**
|
||||||
|
```
|
||||||
|
🔍 Получены данные объекта: {
|
||||||
|
id: 110,
|
||||||
|
name: "Название объекта",
|
||||||
|
collidable: true, ← Проверьте это значение
|
||||||
|
city_id: 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **При обновлении объекта:**
|
||||||
|
```
|
||||||
|
🔄 Обновляем существующий объект с ID: 110
|
||||||
|
🔍 Значение collidable для UPDATE: true ← Проверьте это значение
|
||||||
|
✅ Объект обновлен: { id: 110 }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **При создании объекта:**
|
||||||
|
```
|
||||||
|
🆕 Создаем новый объект
|
||||||
|
🔍 Значение collidable для INSERT: true ← Проверьте это значение
|
||||||
|
✅ Новый объект создан: { id: 111 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Возможные проблемы:
|
||||||
|
|
||||||
|
### **1. Коллизия не обновляется в userData**
|
||||||
|
**Симптом:** `obj.userData.collidable` остается false
|
||||||
|
**Решение:** Проверьте, что вы нажали "Применить" после изменения коллизии
|
||||||
|
|
||||||
|
### **2. Коллизия не передается на сервер**
|
||||||
|
**Симптом:** `objectData.collidable` остается false
|
||||||
|
**Решение:** Проверьте, что `obj.userData.collidable` обновляется
|
||||||
|
|
||||||
|
### **3. Коллизия не сохраняется в БД**
|
||||||
|
**Симптом:** На сервере `collidable` остается false
|
||||||
|
**Решение:** Проверьте SQL запрос и значения параметров
|
||||||
|
|
||||||
|
## 🔧 Пошаговая диагностика:
|
||||||
|
|
||||||
|
### **Шаг 1: Измените коллизию**
|
||||||
|
1. Выберите объект
|
||||||
|
2. Поставьте/снимите галочку "Коллизия"
|
||||||
|
3. Нажмите "Применить"
|
||||||
|
4. Проверьте лог: `🔧 Обновлены свойства объекта`
|
||||||
|
|
||||||
|
### **Шаг 2: Сохраните объект**
|
||||||
|
1. Нажмите "Сохранить"
|
||||||
|
2. Проверьте лог: `🔍 Проверяем коллизию`
|
||||||
|
3. Все три значения должны быть одинаковыми
|
||||||
|
|
||||||
|
### **Шаг 3: Проверьте сервер**
|
||||||
|
1. Посмотрите логи сервера
|
||||||
|
2. Проверьте: `🔍 Получены данные объекта`
|
||||||
|
3. Проверьте: `🔍 Значение collidable для UPDATE/INSERT`
|
||||||
|
|
||||||
|
### **Шаг 4: Проверьте БД**
|
||||||
|
1. Выполните SQL запрос:
|
||||||
|
```sql
|
||||||
|
SELECT id, name, collidable FROM city_objects WHERE id = [ID_ОБЪЕКТА];
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Ожидаемый результат:
|
||||||
|
После выполнения всех шагов коллизия должна сохраняться в БД и загружаться при перезагрузке страницы.
|
||||||
90
DB_TROUBLESHOOTING.md
Normal file
90
DB_TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Диагностика проблем с базой данных
|
||||||
|
|
||||||
|
## Проблема: Сохранение в БД не работает
|
||||||
|
|
||||||
|
### Шаг 1: Проверка таблицы city_objects
|
||||||
|
|
||||||
|
1. Откройте редактор карт
|
||||||
|
2. Нажмите кнопку "Проверить БД"
|
||||||
|
3. Проверьте результат в консоли браузера
|
||||||
|
|
||||||
|
**Если таблица не существует:**
|
||||||
|
```bash
|
||||||
|
node run-city-objects-migration.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 2: Проверка подключения к БД
|
||||||
|
|
||||||
|
Проверьте переменные окружения в `.env`:
|
||||||
|
```env
|
||||||
|
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 3: Проверка логов сервера
|
||||||
|
|
||||||
|
В консоли сервера должны быть логи:
|
||||||
|
```
|
||||||
|
Сохранение объекта: { id: null, city_id: 1, name: "...", ... }
|
||||||
|
Объект сохранен: { id: 123, success: true }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 4: Проверка API эндпоинтов
|
||||||
|
|
||||||
|
Тестовые запросы:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка таблицы
|
||||||
|
curl -X GET http://localhost:3000/api/test-city-objects \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
|
||||||
|
# Проверка сохранения объекта
|
||||||
|
curl -X POST http://localhost:3000/api/save-object \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"city_id": 1,
|
||||||
|
"model_url": "models/copied/test.glb",
|
||||||
|
"name": "Тестовый объект",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 0
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Возможные проблемы и решения
|
||||||
|
|
||||||
|
### 1. Таблица city_objects не существует
|
||||||
|
**Решение:** Выполните миграцию
|
||||||
|
```bash
|
||||||
|
node run-city-objects-migration.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Ошибка подключения к БД
|
||||||
|
**Решение:** Проверьте DATABASE_URL в .env файле
|
||||||
|
|
||||||
|
### 3. Ошибка прав доступа
|
||||||
|
**Решение:** Убедитесь, что пользователь БД имеет права на создание таблиц
|
||||||
|
|
||||||
|
### 4. Ошибка внешних ключей
|
||||||
|
**Решение:** Убедитесь, что существуют таблицы:
|
||||||
|
- `cities`
|
||||||
|
- `organizations`
|
||||||
|
- `interiors`
|
||||||
|
|
||||||
|
### 5. Ошибка аутентификации
|
||||||
|
**Решение:** Проверьте токен в localStorage браузера
|
||||||
|
|
||||||
|
## Отладка в браузере
|
||||||
|
|
||||||
|
1. Откройте DevTools (F12)
|
||||||
|
2. Перейдите на вкладку Network
|
||||||
|
3. Попробуйте добавить объект в редактор
|
||||||
|
4. Найдите запрос к `/api/save-object`
|
||||||
|
5. Проверьте статус ответа и содержимое
|
||||||
|
|
||||||
|
## Логи сервера
|
||||||
|
|
||||||
|
Проверьте консоль сервера на наличие ошибок:
|
||||||
|
- Ошибки подключения к БД
|
||||||
|
- Ошибки SQL запросов
|
||||||
|
- Ошибки валидации данных
|
||||||
91
DEBUG_SAVE_ISSUE.md
Normal file
91
DEBUG_SAVE_ISSUE.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Диагностика проблемы с сохранением объектов
|
||||||
|
|
||||||
|
## 🚨 Проблема
|
||||||
|
Объекты исчезают после перезагрузки страницы - не сохраняются в БД.
|
||||||
|
|
||||||
|
## 🔍 Пошаговая диагностика:
|
||||||
|
|
||||||
|
### **Шаг 1: Проверьте консоль браузера**
|
||||||
|
1. Откройте DevTools (F12)
|
||||||
|
2. Перейдите на вкладку Console
|
||||||
|
3. Добавьте объект в редактор
|
||||||
|
4. Ищите логи с эмодзи:
|
||||||
|
- 🔄 Начинаем сохранение объекта в БД...
|
||||||
|
- 📤 Отправляем данные на сервер
|
||||||
|
- 📡 Ответ сервера: 200 OK
|
||||||
|
- ✅ Объект успешно сохранен в БД
|
||||||
|
|
||||||
|
### **Шаг 2: Проверьте таблицу БД**
|
||||||
|
1. Нажмите кнопку "Проверить БД"
|
||||||
|
2. Должно показать: "Таблица существует: true"
|
||||||
|
3. Если нет - выполните миграцию:
|
||||||
|
```bash
|
||||||
|
node run-minimal-migration.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Шаг 3: Проверьте объекты в БД**
|
||||||
|
1. Нажмите кнопку "Проверить объекты"
|
||||||
|
2. Должно показать количество объектов в БД
|
||||||
|
3. Проверьте консоль для списка объектов
|
||||||
|
|
||||||
|
### **Шаг 4: Проверьте загрузку объектов**
|
||||||
|
1. Перезагрузите страницу
|
||||||
|
2. В консоли ищите логи:
|
||||||
|
- 🔄 Загружаем объекты для города: [ID]
|
||||||
|
- 📊 Загружено объектов из БД: [количество]
|
||||||
|
- ✅ Объект добавлен в сцену: [название]
|
||||||
|
|
||||||
|
## 🐛 Возможные проблемы и решения:
|
||||||
|
|
||||||
|
### **1. Таблица не существует**
|
||||||
|
**Симптомы:** Ошибка 500 при сохранении
|
||||||
|
**Решение:**
|
||||||
|
```bash
|
||||||
|
node run-minimal-migration.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Ошибка аутентификации**
|
||||||
|
**Симптомы:** Ошибка 401 при сохранении
|
||||||
|
**Решение:** Проверьте токен в localStorage
|
||||||
|
|
||||||
|
### **3. Объект сохраняется, но не загружается**
|
||||||
|
**Симптомы:** Логи сохранения есть, но загрузки нет
|
||||||
|
**Решение:** Проверьте cityId при загрузке
|
||||||
|
|
||||||
|
### **4. Ошибка модели**
|
||||||
|
**Симптомы:** Ошибка загрузки GLTF
|
||||||
|
**Решение:** Проверьте путь к модели в model_url
|
||||||
|
|
||||||
|
## 🔧 Дополнительная диагностика:
|
||||||
|
|
||||||
|
### **Проверка API напрямую:**
|
||||||
|
```bash
|
||||||
|
# Проверка таблицы
|
||||||
|
curl -X GET http://localhost:3000/api/test-city-objects \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
|
||||||
|
# Проверка объектов города
|
||||||
|
curl -X GET http://localhost:3000/api/cities/1/objects \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Проверка логов сервера:**
|
||||||
|
В консоли сервера должны быть логи:
|
||||||
|
```
|
||||||
|
Сохранение объекта: { id: null, city_id: 1, ... }
|
||||||
|
Объект сохранен: { id: 123, success: true }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Чек-лист диагностики:
|
||||||
|
|
||||||
|
- [ ] Таблица city_objects существует
|
||||||
|
- [ ] Токен аутентификации валиден
|
||||||
|
- [ ] cityId установлен при сохранении
|
||||||
|
- [ ] Объект сохраняется в БД (логи в консоли)
|
||||||
|
- [ ] Объекты загружаются при перезагрузке
|
||||||
|
- [ ] Модели загружаются корректно
|
||||||
|
- [ ] Нет ошибок в консоли браузера
|
||||||
|
- [ ] Нет ошибок в логах сервера
|
||||||
|
|
||||||
|
## 🎯 Ожидаемый результат:
|
||||||
|
После выполнения всех проверок объекты должны сохраняться и загружаться корректно.
|
||||||
65
DUPLICATE_KEY_FIX.md
Normal file
65
DUPLICATE_KEY_FIX.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Исправление ошибки дублирования ключей
|
||||||
|
|
||||||
|
## 🚨 Проблема
|
||||||
|
Ошибка: `duplicate key value violates unique constraint "city_objects_pkey"`
|
||||||
|
- Объекты загружаются из БД с существующими ID
|
||||||
|
- При сохранении пытаемся создать объекты с теми же ID
|
||||||
|
- Это вызывает конфликт первичных ключей
|
||||||
|
|
||||||
|
## ✅ Решение
|
||||||
|
|
||||||
|
### **1. Исправлена логика на клиенте:**
|
||||||
|
- Если у объекта есть ID → обновляем существующий
|
||||||
|
- Если у объекта нет ID → создаем новый
|
||||||
|
- Добавлено логирование для отладки
|
||||||
|
|
||||||
|
### **2. Исправлена логика на сервере:**
|
||||||
|
- Проверка `if (id && id !== null && id !== undefined)`
|
||||||
|
- Если ID есть → UPDATE (обновление)
|
||||||
|
- Если ID нет → INSERT (создание)
|
||||||
|
- Добавлено подробное логирование
|
||||||
|
|
||||||
|
## 🔧 Как это работает:
|
||||||
|
|
||||||
|
### **Для существующих объектов:**
|
||||||
|
1. Объект загружается из БД с ID (например, 110)
|
||||||
|
2. Пользователь редактирует объект
|
||||||
|
3. При сохранении отправляется ID = 110
|
||||||
|
4. Сервер выполняет UPDATE для ID = 110
|
||||||
|
5. ✅ Объект обновлен
|
||||||
|
|
||||||
|
### **Для новых объектов:**
|
||||||
|
1. Пользователь добавляет новый объект
|
||||||
|
2. У объекта нет ID (null)
|
||||||
|
3. При сохранении отправляется ID = null
|
||||||
|
4. Сервер выполняет INSERT без указания ID
|
||||||
|
5. ✅ Новый объект создан с автоматическим ID
|
||||||
|
|
||||||
|
## 📋 Логи для отладки:
|
||||||
|
|
||||||
|
### **В консоли браузера:**
|
||||||
|
```
|
||||||
|
🔄 Начинаем сохранение объекта в БД...
|
||||||
|
📝 Обновляем объект: Название объекта
|
||||||
|
📤 Отправляем данные на сервер: { id: 110, ... }
|
||||||
|
📡 Ответ сервера: 200 OK
|
||||||
|
✅ Объект успешно сохранен в БД: { id: 110, success: true }
|
||||||
|
```
|
||||||
|
|
||||||
|
### **В консоли сервера:**
|
||||||
|
```
|
||||||
|
🔄 Обновляем существующий объект с ID: 110
|
||||||
|
✅ Объект обновлен: { id: 110 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Результат:
|
||||||
|
- Существующие объекты обновляются
|
||||||
|
- Новые объекты создаются
|
||||||
|
- Нет конфликтов первичных ключей
|
||||||
|
- Все операции логируются для отладки
|
||||||
|
|
||||||
|
## 🔍 Проверка:
|
||||||
|
1. Добавьте объект → нажмите "Сохранить"
|
||||||
|
2. Отредактируйте объект → нажмите "Сохранить"
|
||||||
|
3. Перезагрузите страницу → объекты должны загрузиться
|
||||||
|
4. Проверьте логи в консоли браузера и сервера
|
||||||
81
FIXES_APPLIED.md
Normal file
81
FIXES_APPLIED.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Исправления проблем редактора карт
|
||||||
|
|
||||||
|
## ✅ Исправленные проблемы:
|
||||||
|
|
||||||
|
### 1. **Ошибка `scale.x.toFixed is not a function`**
|
||||||
|
**Проблема:** Объекты Three.js могут иметь неинициализированные свойства scale
|
||||||
|
**Решение:** Добавлена проверка типов перед вызовом `.toFixed()`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Безопасное получение масштаба
|
||||||
|
const scaleX = typeof obj.scale.x === 'number' ? obj.scale.x : 1;
|
||||||
|
const scaleY = typeof obj.scale.y === 'number' ? obj.scale.y : 1;
|
||||||
|
const scaleZ = typeof obj.scale.z === 'number' ? obj.scale.z : 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Ошибка миграции `column "created_at" does not exist`**
|
||||||
|
**Проблема:** Триггер пытался обратиться к колонке до её создания
|
||||||
|
**Решение:** Создана простая миграция без триггеров
|
||||||
|
|
||||||
|
## 🚀 Инструкции по запуску:
|
||||||
|
|
||||||
|
### **Для исправления ошибки scale:**
|
||||||
|
Код уже исправлен в `MapEditor.jsx`. Ошибка больше не должна возникать.
|
||||||
|
|
||||||
|
### **Для исправления миграции:**
|
||||||
|
```bash
|
||||||
|
# Используйте простую миграцию вместо сложной
|
||||||
|
node run-simple-migration.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Проверка результата:**
|
||||||
|
1. Откройте редактор карт
|
||||||
|
2. Нажмите кнопку "Проверить БД"
|
||||||
|
3. Должно показать: "Таблица существует: true"
|
||||||
|
|
||||||
|
## 📋 Что было исправлено:
|
||||||
|
|
||||||
|
### **В MapEditor.jsx:**
|
||||||
|
- ✅ Безопасная обработка координат, поворота и масштаба
|
||||||
|
- ✅ Проверка типов перед вызовом `.toFixed()`
|
||||||
|
- ✅ Значения по умолчанию для неинициализированных свойств
|
||||||
|
|
||||||
|
### **В миграции:**
|
||||||
|
- ✅ Создана простая версия без триггеров
|
||||||
|
- ✅ Убраны сложные зависимости
|
||||||
|
- ✅ Добавлена проверка существования таблицы
|
||||||
|
|
||||||
|
## 🔧 Альтернативные решения:
|
||||||
|
|
||||||
|
### **Если простая миграция не работает:**
|
||||||
|
```sql
|
||||||
|
-- Создайте таблицу вручную в pgAdmin или psql
|
||||||
|
CREATE TABLE city_objects (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
city_id INTEGER NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
model_url VARCHAR(500) NOT NULL,
|
||||||
|
pos_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_z DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_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,
|
||||||
|
organization_id INTEGER NOT NULL DEFAULT 2,
|
||||||
|
rent DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
tax DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
collidable BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
interior_id INTEGER NOT NULL DEFAULT 101,
|
||||||
|
textures VARCHAR(500) NOT NULL DEFAULT '-',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Результат:
|
||||||
|
- Ошибки `scale.x.toFixed` больше не возникают
|
||||||
|
- Миграция выполняется без ошибок
|
||||||
|
- Редактор карт работает стабильно
|
||||||
117
GALLERY_FIXES.md
Normal file
117
GALLERY_FIXES.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Исправления галереи моделей
|
||||||
|
|
||||||
|
## 🔧 Проблемы и решения
|
||||||
|
|
||||||
|
### **1. Модельки не подгружаются (меши)**
|
||||||
|
|
||||||
|
#### **Проблема:**
|
||||||
|
- Модели не отображались с правильными мешами
|
||||||
|
- Ошибки загрузки GLTF файлов
|
||||||
|
|
||||||
|
#### **Решение:**
|
||||||
|
- ✅ Добавлено подробное логирование загрузки
|
||||||
|
- ✅ Улучшена обработка ошибок
|
||||||
|
- ✅ Проверка путей к моделям
|
||||||
|
- ✅ Логирование URL модели при ошибке
|
||||||
|
|
||||||
|
#### **Код:**
|
||||||
|
```javascript
|
||||||
|
console.log('🔄 Загружаем модель:', modelUrl);
|
||||||
|
console.log('✅ Модель загружена:', gltf);
|
||||||
|
console.error('❌ Ошибка загрузки модели:', error);
|
||||||
|
console.error('URL модели:', modelUrl);
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Слишком много моделей - PNG превью**
|
||||||
|
|
||||||
|
#### **Проблема:**
|
||||||
|
- 3D рендеринг всех моделей одновременно замедлял страницу
|
||||||
|
- Высокая нагрузка на GPU и память
|
||||||
|
|
||||||
|
#### **Решение:**
|
||||||
|
- ✅ Создан компонент `ModelThumbnail.jsx` для генерации PNG превью
|
||||||
|
- ✅ Кэширование превью в localStorage
|
||||||
|
- ✅ 3D рендеринг только при детальном просмотре
|
||||||
|
- ✅ Автоматическое центрирование и масштабирование
|
||||||
|
|
||||||
|
#### **Особенности PNG превью:**
|
||||||
|
- Размер: 300x300px
|
||||||
|
- Формат: PNG с прозрачностью
|
||||||
|
- Кэширование в localStorage
|
||||||
|
- Автоматическая генерация при первом просмотре
|
||||||
|
|
||||||
|
### **3. Страница не листается вниз**
|
||||||
|
|
||||||
|
#### **Проблема:**
|
||||||
|
- Отсутствие прокрутки на странице галереи
|
||||||
|
- Фиксированная высота контейнера
|
||||||
|
|
||||||
|
#### **Решение:**
|
||||||
|
- ✅ Добавлен `minHeight: '100vh'`
|
||||||
|
- ✅ Добавлен `overflowY: 'auto'`
|
||||||
|
- ✅ Исправлена структура контейнеров
|
||||||
|
|
||||||
|
#### **CSS:**
|
||||||
|
```css
|
||||||
|
minHeight: '100vh',
|
||||||
|
overflowY: 'auto'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Новые возможности
|
||||||
|
|
||||||
|
### **ModelThumbnail.jsx:**
|
||||||
|
- Генерация PNG превью моделей
|
||||||
|
- Кэширование в localStorage
|
||||||
|
- Обработка ошибок загрузки
|
||||||
|
- Анимация загрузки
|
||||||
|
|
||||||
|
### **ModelGallery.jsx:**
|
||||||
|
- Использование PNG превью в сетке
|
||||||
|
- 3D превью только в модальном окне
|
||||||
|
- Исправленная прокрутка
|
||||||
|
- CSS анимации
|
||||||
|
|
||||||
|
## 📊 Производительность
|
||||||
|
|
||||||
|
### **До исправлений:**
|
||||||
|
- ❌ Все модели рендерились в 3D
|
||||||
|
- ❌ Высокая нагрузка на GPU
|
||||||
|
- ❌ Медленная загрузка страницы
|
||||||
|
- ❌ Нет прокрутки
|
||||||
|
|
||||||
|
### **После исправлений:**
|
||||||
|
- ✅ PNG превью для быстрого просмотра
|
||||||
|
- ✅ 3D рендеринг только при необходимости
|
||||||
|
- ✅ Кэширование превью
|
||||||
|
- ✅ Полная прокрутка страницы
|
||||||
|
- ✅ Быстрая загрузка
|
||||||
|
|
||||||
|
## 🔧 Технические детали
|
||||||
|
|
||||||
|
### **Генерация PNG превью:**
|
||||||
|
1. Создается временная Three.js сцена
|
||||||
|
2. Загружается модель
|
||||||
|
3. Рендерится в canvas 300x300px
|
||||||
|
4. Конвертируется в PNG
|
||||||
|
5. Сохраняется в localStorage
|
||||||
|
6. Сцена очищается
|
||||||
|
|
||||||
|
### **Кэширование:**
|
||||||
|
```javascript
|
||||||
|
const cacheKey = `model_thumb_${modelName}`;
|
||||||
|
const cachedImage = localStorage.getItem(cacheKey);
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Обработка ошибок:**
|
||||||
|
- Логирование URL модели
|
||||||
|
- Детальные сообщения об ошибках
|
||||||
|
- Fallback для недоступных моделей
|
||||||
|
|
||||||
|
## 🎯 Результат
|
||||||
|
|
||||||
|
- ✅ Быстрая загрузка галереи
|
||||||
|
- ✅ PNG превью всех моделей
|
||||||
|
- ✅ Полная прокрутка страницы
|
||||||
|
- ✅ 3D просмотр при необходимости
|
||||||
|
- ✅ Кэширование превью
|
||||||
|
- ✅ Обработка ошибок
|
||||||
160
MAP_EDITOR_API_SETUP.md
Normal file
160
MAP_EDITOR_API_SETUP.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Настройка API для редактора карт
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Добавлены новые API эндпоинты для работы с объектами городов в редакторе карт:
|
||||||
|
|
||||||
|
- `POST /api/save-object` - Сохранение/обновление объекта города
|
||||||
|
- `DELETE /api/delete-object/:id` - Удаление объекта города
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
### 1. Создание таблицы city_objects
|
||||||
|
|
||||||
|
Выполните миграцию для создания таблицы:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node run-city-objects-migration.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Проверка структуры БД
|
||||||
|
|
||||||
|
Убедитесь, что в базе данных существуют связанные таблицы:
|
||||||
|
- `cities` - города
|
||||||
|
- `organizations` - организации
|
||||||
|
- `interiors` - интерьеры
|
||||||
|
|
||||||
|
### 3. Перезапуск сервера
|
||||||
|
|
||||||
|
После добавления новых API эндпоинтов перезапустите сервер:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
# или
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Эндпоинты
|
||||||
|
|
||||||
|
### POST /api/save-object
|
||||||
|
|
||||||
|
Сохранение или обновление объекта города.
|
||||||
|
|
||||||
|
**Параметры запроса:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 123, // ID объекта (для обновления) или null (для создания)
|
||||||
|
"city_id": 1, // ID города (обязательно)
|
||||||
|
"name": "Здание отеля", // Название объекта
|
||||||
|
"model_url": "models/copied/building-hotel.glb", // URL модели (обязательно)
|
||||||
|
"pos_x": 10.5, // X координата
|
||||||
|
"pos_y": 0.0, // Y координата
|
||||||
|
"pos_z": 15.2, // Z координата
|
||||||
|
"rot_x": 0.0, // X поворот (радианы)
|
||||||
|
"rot_y": 1.57, // Y поворот (радианы)
|
||||||
|
"rot_z": 0.0, // Z поворот (радианы)
|
||||||
|
"scale_x": 1.0, // X масштаб
|
||||||
|
"scale_y": 1.0, // Y масштаб
|
||||||
|
"scale_z": 1.0, // Z масштаб
|
||||||
|
"organization_id": 2, // ID организации (по умолчанию 2)
|
||||||
|
"rent": 0, // Аренда (по умолчанию 0)
|
||||||
|
"tax": 0, // Налог (по умолчанию 0)
|
||||||
|
"collidable": false, // Коллизия (по умолчанию false)
|
||||||
|
"interior_id": 101, // ID интерьера (по умолчанию 101)
|
||||||
|
"textures": "-" // Текстуры (по умолчанию "-")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DELETE /api/delete-object/:id
|
||||||
|
|
||||||
|
Удаление объекта города.
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
- `id` - ID объекта для удаления
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Объект удален"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура таблицы city_objects
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE city_objects (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
city_id INTEGER NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
model_url VARCHAR(500) NOT NULL,
|
||||||
|
|
||||||
|
-- Позиция
|
||||||
|
pos_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_z DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Поворот (радианы)
|
||||||
|
rot_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_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,
|
||||||
|
|
||||||
|
-- Дополнительные свойства
|
||||||
|
organization_id INTEGER NOT NULL DEFAULT 2,
|
||||||
|
rent DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
tax DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
collidable BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
interior_id INTEGER NOT NULL DEFAULT 101,
|
||||||
|
textures VARCHAR(500) NOT NULL DEFAULT '-',
|
||||||
|
|
||||||
|
-- Метаданные
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Интеграция с редактором карт
|
||||||
|
|
||||||
|
Редактор карт теперь автоматически:
|
||||||
|
|
||||||
|
1. **Сохраняет объекты** при добавлении новых
|
||||||
|
2. **Обновляет объекты** при изменении координат/свойств
|
||||||
|
3. **Удаляет объекты** из БД при удалении
|
||||||
|
4. **Загружает объекты** при смене города
|
||||||
|
5. **Сохраняет ID** существующих объектов
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- Все эндпоинты требуют аутентификации (`authenticate` middleware)
|
||||||
|
- Валидация входных данных
|
||||||
|
- Обработка ошибок с логированием
|
||||||
|
- SQL injection защита через параметризованные запросы
|
||||||
|
|
||||||
|
## Отладка
|
||||||
|
|
||||||
|
Для проверки работы API используйте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка создания объекта
|
||||||
|
curl -X POST http://localhost:3000/api/save-object \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{"city_id": 1, "model_url": "models/copied/building.glb", "name": "Тестовое здание"}'
|
||||||
|
|
||||||
|
# Проверка удаления объекта
|
||||||
|
curl -X DELETE http://localhost:3000/api/delete-object/123 \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
101
MIGRATION_FIX.md
Normal file
101
MIGRATION_FIX.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Исправление проблемы с миграцией
|
||||||
|
|
||||||
|
## 🚨 Проблема
|
||||||
|
Ошибка: `column "created_at" of relation "city_objects" does not exist`
|
||||||
|
|
||||||
|
## 🔧 Решения (по порядку приоритета):
|
||||||
|
|
||||||
|
### **Решение 1: Минимальная миграция**
|
||||||
|
```bash
|
||||||
|
node run-minimal-migration.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Решение 2: Прямое создание таблицы**
|
||||||
|
1. Откройте pgAdmin или подключитесь к БД через psql
|
||||||
|
2. Выполните SQL из файла `create-table-direct.sql`
|
||||||
|
3. Или скопируйте и выполните этот код:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DROP TABLE IF EXISTS city_objects CASCADE;
|
||||||
|
|
||||||
|
CREATE TABLE city_objects (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
city_id INTEGER NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
model_url VARCHAR(500) NOT NULL,
|
||||||
|
pos_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_z DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_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,
|
||||||
|
organization_id INTEGER NOT NULL DEFAULT 2,
|
||||||
|
rent DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
tax DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
collidable BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
interior_id INTEGER NOT NULL DEFAULT 101,
|
||||||
|
textures VARCHAR(500) NOT NULL DEFAULT '-'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_city_objects_city_id ON city_objects(city_id);
|
||||||
|
CREATE INDEX idx_city_objects_organization_id ON city_objects(organization_id);
|
||||||
|
CREATE INDEX idx_city_objects_interior_id ON city_objects(interior_id);
|
||||||
|
CREATE INDEX idx_city_objects_position ON city_objects(pos_x, pos_y, pos_z);
|
||||||
|
CREATE INDEX idx_city_objects_collidable ON city_objects(collidable);
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Решение 3: Проверка через редактор**
|
||||||
|
1. Откройте редактор карт
|
||||||
|
2. Нажмите кнопку "Проверить БД"
|
||||||
|
3. Если таблица существует, проблема решена
|
||||||
|
|
||||||
|
## ✅ После создания таблицы:
|
||||||
|
|
||||||
|
1. **Перезапустите сервер:**
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Проверьте работу редактора:**
|
||||||
|
- Откройте редактор карт
|
||||||
|
- Попробуйте добавить объект
|
||||||
|
- Проверьте сохранение в БД
|
||||||
|
|
||||||
|
## 🔍 Диагностика:
|
||||||
|
|
||||||
|
### **Проверка таблицы:**
|
||||||
|
```sql
|
||||||
|
SELECT table_name, column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'city_objects'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Проверка записей:**
|
||||||
|
```sql
|
||||||
|
SELECT COUNT(*) FROM city_objects;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Структура таблицы:
|
||||||
|
|
||||||
|
| Колонка | Тип | Описание |
|
||||||
|
|---------|-----|----------|
|
||||||
|
| id | SERIAL | Primary Key |
|
||||||
|
| city_id | INTEGER | ID города |
|
||||||
|
| name | VARCHAR(255) | Название объекта |
|
||||||
|
| model_url | VARCHAR(500) | URL модели |
|
||||||
|
| pos_x, pos_y, pos_z | DECIMAL(15,6) | Координаты |
|
||||||
|
| rot_x, rot_y, rot_z | DECIMAL(15,6) | Поворот |
|
||||||
|
| scale_x, scale_y, scale_z | DECIMAL(15,6) | Масштаб |
|
||||||
|
| organization_id | INTEGER | ID организации |
|
||||||
|
| rent | DECIMAL(10,2) | Аренда |
|
||||||
|
| tax | DECIMAL(10,2) | Налог |
|
||||||
|
| collidable | BOOLEAN | Коллизия |
|
||||||
|
| interior_id | INTEGER | ID интерьера |
|
||||||
|
| textures | VARCHAR(500) | Текстуры |
|
||||||
|
|
||||||
|
## 🎯 Результат:
|
||||||
|
После выполнения любого из решений редактор карт должен работать корректно с сохранением в БД.
|
||||||
317
MIGRATION_GUIDE.md
Normal file
317
MIGRATION_GUIDE.md
Normal 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. Внесите вклад в развитие проекта
|
||||||
|
|
||||||
|
Удачи в разработке! 🚀
|
||||||
133
MODEL_GALLERY_GUIDE.md
Normal file
133
MODEL_GALLERY_GUIDE.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Галерея моделей - Руководство пользователя
|
||||||
|
|
||||||
|
## 🎯 Описание
|
||||||
|
Галерея моделей - это интерактивная страница для просмотра всех доступных 3D моделей с правильными мешами и текстурами.
|
||||||
|
|
||||||
|
## 🚀 Доступ к галерее
|
||||||
|
- **URL:** `/model-gallery`
|
||||||
|
- **Из редактора карт:** Кнопка "Галерея моделей" (открывается в новой вкладке)
|
||||||
|
|
||||||
|
## ✨ Возможности
|
||||||
|
|
||||||
|
### **3D Превью моделей**
|
||||||
|
- ✅ Полноценное 3D отображение с правильными мешами
|
||||||
|
- ✅ Автоматическое центрирование и масштабирование
|
||||||
|
- ✅ Интерактивное управление (поворот, масштабирование)
|
||||||
|
- ✅ Правильное освещение и тени
|
||||||
|
- ✅ Загрузка текстур и материалов
|
||||||
|
|
||||||
|
### **Навигация и поиск**
|
||||||
|
- 🔍 Поиск по названию модели
|
||||||
|
- 📊 Счетчик найденных моделей
|
||||||
|
- 🎯 Фильтрация в реальном времени
|
||||||
|
|
||||||
|
### **Интерактивность**
|
||||||
|
- 🖱️ Клик по модели для детального просмотра
|
||||||
|
- 🔄 Модальное окно с увеличенным превью
|
||||||
|
- 📥 Кнопки "Использовать" и "Скачать"
|
||||||
|
- ⌨️ Управление мышью и клавиатурой
|
||||||
|
|
||||||
|
## 🎮 Управление
|
||||||
|
|
||||||
|
### **В 3D превью:**
|
||||||
|
- **ЛКМ + перетаскивание** - поворот камеры
|
||||||
|
- **Колесо мыши** - масштабирование
|
||||||
|
- **ПКМ + перетаскивание** - панорамирование
|
||||||
|
|
||||||
|
### **В интерфейсе:**
|
||||||
|
- **Поиск** - введите название модели
|
||||||
|
- **Клик по карточке** - детальный просмотр
|
||||||
|
- **Кнопка "Использовать"** - выбор для редактора
|
||||||
|
- **Кнопка "Скачать"** - скачивание файла
|
||||||
|
|
||||||
|
## 🏗️ Технические детали
|
||||||
|
|
||||||
|
### **Компоненты:**
|
||||||
|
- `ModelPreview.jsx` - 3D превью одной модели
|
||||||
|
- `ModelGallery.jsx` - основная страница галереи
|
||||||
|
- API endpoint: `GET /api/models`
|
||||||
|
|
||||||
|
### **Поддерживаемые форматы:**
|
||||||
|
- `.glb` (GLTF Binary)
|
||||||
|
- `.gltf` (GLTF)
|
||||||
|
|
||||||
|
### **Оптимизации:**
|
||||||
|
- Автоматическое масштабирование под размер контейнера
|
||||||
|
- Центрирование модели в сцене
|
||||||
|
- Оптимизация материалов и текстур
|
||||||
|
- Тени и освещение для лучшего восприятия
|
||||||
|
|
||||||
|
## 📁 Структура файлов
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/
|
||||||
|
│ └── ModelPreview.jsx # 3D превью компонент
|
||||||
|
├── pages/
|
||||||
|
│ └── ModelGallery.jsx # Основная страница галереи
|
||||||
|
└── App.js # Маршрутизация
|
||||||
|
|
||||||
|
public/
|
||||||
|
└── models/
|
||||||
|
└── copied/ # Папка с моделями
|
||||||
|
├── building-*.glb
|
||||||
|
├── vehicle-*.glb
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 API Endpoints
|
||||||
|
|
||||||
|
### **GET /api/models**
|
||||||
|
Возвращает список всех доступных моделей.
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"building-hotel.glb",
|
||||||
|
"vehicle-car.glb",
|
||||||
|
"furniture-chair.glb"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Стилизация
|
||||||
|
|
||||||
|
### **Карточка модели:**
|
||||||
|
- Размер: 300px минимальная ширина
|
||||||
|
- Тень и скругленные углы
|
||||||
|
- Hover эффекты
|
||||||
|
- Выделение выбранной модели
|
||||||
|
|
||||||
|
### **Модальное окно:**
|
||||||
|
- Полноэкранный просмотр
|
||||||
|
- Кнопка закрытия
|
||||||
|
- Действия с моделью
|
||||||
|
|
||||||
|
## 🚀 Использование в редакторе
|
||||||
|
|
||||||
|
1. Откройте галерею моделей
|
||||||
|
2. Найдите нужную модель
|
||||||
|
3. Нажмите "Использовать в редакторе"
|
||||||
|
4. Модель будет добавлена в редактор карт
|
||||||
|
|
||||||
|
## 🐛 Возможные проблемы
|
||||||
|
|
||||||
|
### **Модель не загружается:**
|
||||||
|
- Проверьте, что файл существует в `/public/models/copied/`
|
||||||
|
- Убедитесь, что формат файла поддерживается (.glb, .gltf)
|
||||||
|
|
||||||
|
### **Медленная загрузка:**
|
||||||
|
- Модели загружаются по требованию
|
||||||
|
- Большие файлы могут загружаться дольше
|
||||||
|
|
||||||
|
### **Проблемы с отображением:**
|
||||||
|
- Проверьте консоль браузера на ошибки
|
||||||
|
- Убедитесь, что Three.js загружен корректно
|
||||||
|
|
||||||
|
## 📈 Планы развития
|
||||||
|
|
||||||
|
- [ ] Категоризация моделей
|
||||||
|
- [ ] Предварительный просмотр анимаций
|
||||||
|
- [ ] Информация о полигонах и размере файла
|
||||||
|
- [ ] Избранные модели
|
||||||
|
- [ ] Сравнение моделей
|
||||||
|
- [ ] Пакетная загрузка моделей
|
||||||
88
NEW_SAVE_MECHANISM.md
Normal file
88
NEW_SAVE_MECHANISM.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Новый механизм сохранения объектов
|
||||||
|
|
||||||
|
## 🔄 Изменения в системе сохранения
|
||||||
|
|
||||||
|
### **Было:**
|
||||||
|
- Объекты автоматически сохранялись в БД при добавлении/изменении
|
||||||
|
- Дополнительно сохранялись в файлы в папке `saves/`
|
||||||
|
- Кнопка "Сохранить" сохраняла в файл
|
||||||
|
|
||||||
|
### **Стало:**
|
||||||
|
- Объекты НЕ сохраняются автоматически
|
||||||
|
- Сохранение происходит ТОЛЬКО при нажатии кнопки "Сохранить"
|
||||||
|
- Сохранение происходит ТОЛЬКО в базу данных
|
||||||
|
- Файлы в папке `saves/` больше не создаются
|
||||||
|
|
||||||
|
## 🎯 Новый рабочий процесс:
|
||||||
|
|
||||||
|
### **1. Добавление объектов:**
|
||||||
|
- Выберите модель из списка
|
||||||
|
- Нажмите "Добавить"
|
||||||
|
- Объект появится в сцене
|
||||||
|
- **НО:** объект НЕ сохраняется в БД автоматически
|
||||||
|
|
||||||
|
### **2. Редактирование объектов:**
|
||||||
|
- Выберите объект
|
||||||
|
- Измените координаты/свойства
|
||||||
|
- Нажмите "Применить"
|
||||||
|
- **НО:** изменения НЕ сохраняются в БД автоматически
|
||||||
|
|
||||||
|
### **3. Сохранение в БД:**
|
||||||
|
- Нажмите кнопку "Сохранить"
|
||||||
|
- Все объекты сохранятся в базу данных
|
||||||
|
- Появится сообщение "Карта сохранена в базу данных"
|
||||||
|
|
||||||
|
### **4. Загрузка при перезагрузке:**
|
||||||
|
- Объекты загружаются из БД
|
||||||
|
- Файлы из папки `saves/` игнорируются
|
||||||
|
|
||||||
|
## 🔧 Преимущества нового механизма:
|
||||||
|
|
||||||
|
### **1. Контролируемое сохранение:**
|
||||||
|
- Вы решаете, когда сохранять
|
||||||
|
- Можно сделать много изменений и сохранить одним кликом
|
||||||
|
|
||||||
|
### **2. Надежность:**
|
||||||
|
- Все данные в БД
|
||||||
|
- Нет дублирования в файлах
|
||||||
|
- Единый источник истины
|
||||||
|
|
||||||
|
### **3. Производительность:**
|
||||||
|
- Нет лишних запросов к БД
|
||||||
|
- Сохранение только при необходимости
|
||||||
|
|
||||||
|
## 📋 Инструкция для пользователя:
|
||||||
|
|
||||||
|
### **Для добавления объектов:**
|
||||||
|
1. Выберите город
|
||||||
|
2. Выберите модель
|
||||||
|
3. Нажмите "Добавить"
|
||||||
|
4. Отредактируйте при необходимости
|
||||||
|
5. **Нажмите "Сохранить"** ← Важно!
|
||||||
|
|
||||||
|
### **Для редактирования:**
|
||||||
|
1. Выберите объект
|
||||||
|
2. Измените координаты/свойства
|
||||||
|
3. Нажмите "Применить"
|
||||||
|
4. **Нажмите "Сохранить"** ← Важно!
|
||||||
|
|
||||||
|
### **Для проверки сохранения:**
|
||||||
|
1. Нажмите "Проверить объекты"
|
||||||
|
2. Должно показать количество объектов в БД
|
||||||
|
3. Перезагрузите страницу
|
||||||
|
4. Объекты должны загрузиться из БД
|
||||||
|
|
||||||
|
## ⚠️ Важные моменты:
|
||||||
|
|
||||||
|
### **Объекты НЕ сохраняются автоматически!**
|
||||||
|
- При добавлении объекта - он только в памяти
|
||||||
|
- При изменении объекта - изменения только в памяти
|
||||||
|
- **Только кнопка "Сохранить" записывает в БД**
|
||||||
|
|
||||||
|
### **Проверка сохранения:**
|
||||||
|
- Используйте кнопку "Проверить объекты"
|
||||||
|
- Перезагрузите страницу для проверки
|
||||||
|
- Объекты должны остаться после перезагрузки
|
||||||
|
|
||||||
|
## 🎯 Результат:
|
||||||
|
Теперь все объекты сохраняются только в базу данных, а файлы в папке `saves/` больше не создаются.
|
||||||
32
create-table-direct.sql
Normal file
32
create-table-direct.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
-- Прямое создание таблицы city_objects
|
||||||
|
-- Выполните этот SQL в pgAdmin или psql
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS city_objects CASCADE;
|
||||||
|
|
||||||
|
CREATE TABLE city_objects (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
city_id INTEGER NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
model_url VARCHAR(500) NOT NULL,
|
||||||
|
pos_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_z DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_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,
|
||||||
|
organization_id INTEGER NOT NULL DEFAULT 2,
|
||||||
|
rent DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
tax DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
collidable BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
interior_id INTEGER NOT NULL DEFAULT 101,
|
||||||
|
textures VARCHAR(500) NOT NULL DEFAULT '-'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_city_objects_city_id ON city_objects(city_id);
|
||||||
|
CREATE INDEX idx_city_objects_organization_id ON city_objects(organization_id);
|
||||||
|
CREATE INDEX idx_city_objects_interior_id ON city_objects(interior_id);
|
||||||
|
CREATE INDEX idx_city_objects_position ON city_objects(pos_x, pos_y, pos_z);
|
||||||
|
CREATE INDEX idx_city_objects_collidable ON city_objects(collidable);
|
||||||
60
daily_availability.py
Normal file
60
daily_availability.py
Normal 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
15
db1.js
@@ -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
48
db2.js
Normal 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
60
migrate-colliders.js
Normal 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
113
migrate-json-to-db.js
Normal 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);
|
||||||
|
});
|
||||||
94
migrations/create_city_objects_table.sql
Normal file
94
migrations/create_city_objects_table.sql
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
-- Миграция для создания таблицы city_objects
|
||||||
|
-- Файл: migrations/create_city_objects_table.sql
|
||||||
|
|
||||||
|
-- Создание таблицы city_objects
|
||||||
|
CREATE TABLE IF NOT EXISTS city_objects (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
city_id INTEGER NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
model_url VARCHAR(500) NOT NULL,
|
||||||
|
|
||||||
|
-- Позиция
|
||||||
|
pos_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_z DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Поворот (в радианах)
|
||||||
|
rot_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_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,
|
||||||
|
|
||||||
|
-- Дополнительные свойства
|
||||||
|
organization_id INTEGER NOT NULL DEFAULT 2,
|
||||||
|
rent DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
tax DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
collidable BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
interior_id INTEGER NOT NULL DEFAULT 101,
|
||||||
|
textures VARCHAR(500) NOT NULL DEFAULT '-',
|
||||||
|
|
||||||
|
-- Метаданные
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- Внешние ключи
|
||||||
|
CONSTRAINT fk_city_objects_city FOREIGN KEY (city_id) REFERENCES cities(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_city_objects_organization FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE SET DEFAULT,
|
||||||
|
CONSTRAINT fk_city_objects_interior FOREIGN KEY (interior_id) REFERENCES interiors(id) ON DELETE SET DEFAULT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Создание индексов для оптимизации запросов
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_city_objects_city_id ON city_objects(city_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_city_objects_organization_id ON city_objects(organization_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_city_objects_interior_id ON city_objects(interior_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_city_objects_position ON city_objects(pos_x, pos_y, pos_z);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_city_objects_collidable ON city_objects(collidable);
|
||||||
|
|
||||||
|
-- Создание функции для автоматического обновления updated_at
|
||||||
|
CREATE OR REPLACE FUNCTION update_city_objects_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ language 'plpgsql';
|
||||||
|
|
||||||
|
-- Создание триггера для автоматического обновления updated_at (только после создания таблицы)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'city_objects') THEN
|
||||||
|
DROP TRIGGER IF EXISTS update_city_objects_updated_at ON city_objects;
|
||||||
|
CREATE TRIGGER update_city_objects_updated_at
|
||||||
|
BEFORE UPDATE ON city_objects
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_city_objects_updated_at();
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Комментарии к таблице и колонкам
|
||||||
|
COMMENT ON TABLE city_objects IS 'Таблица объектов городов';
|
||||||
|
COMMENT ON COLUMN city_objects.id IS 'Уникальный идентификатор объекта';
|
||||||
|
COMMENT ON COLUMN city_objects.city_id IS 'ID города, к которому принадлежит объект';
|
||||||
|
COMMENT ON COLUMN city_objects.name IS 'Название объекта';
|
||||||
|
COMMENT ON COLUMN city_objects.model_url IS 'URL модели объекта';
|
||||||
|
COMMENT ON COLUMN city_objects.pos_x IS 'X координата позиции';
|
||||||
|
COMMENT ON COLUMN city_objects.pos_y IS 'Y координата позиции';
|
||||||
|
COMMENT ON COLUMN city_objects.pos_z IS 'Z координата позиции';
|
||||||
|
COMMENT ON COLUMN city_objects.rot_x IS 'X компонент поворота (радианы)';
|
||||||
|
COMMENT ON COLUMN city_objects.rot_y IS 'Y компонент поворота (радианы)';
|
||||||
|
COMMENT ON COLUMN city_objects.rot_z IS 'Z компонент поворота (радианы)';
|
||||||
|
COMMENT ON COLUMN city_objects.scale_x IS 'X компонент масштаба';
|
||||||
|
COMMENT ON COLUMN city_objects.scale_y IS 'Y компонент масштаба';
|
||||||
|
COMMENT ON COLUMN city_objects.scale_z IS 'Z компонент масштаба';
|
||||||
|
COMMENT ON COLUMN city_objects.organization_id IS 'ID организации-владельца';
|
||||||
|
COMMENT ON COLUMN city_objects.rent IS 'Стоимость аренды';
|
||||||
|
COMMENT ON COLUMN city_objects.tax IS 'Налог';
|
||||||
|
COMMENT ON COLUMN city_objects.collidable IS 'Имеет ли объект коллизию';
|
||||||
|
COMMENT ON COLUMN city_objects.interior_id IS 'ID интерьера объекта';
|
||||||
|
COMMENT ON COLUMN city_objects.textures IS 'Информация о текстурах';
|
||||||
|
COMMENT ON COLUMN city_objects.created_at IS 'Время создания записи';
|
||||||
|
COMMENT ON COLUMN city_objects.updated_at IS 'Время последнего обновления записи';
|
||||||
43
migrations/create_city_objects_table_minimal.sql
Normal file
43
migrations/create_city_objects_table_minimal.sql
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
-- Минимальная миграция для создания таблицы city_objects
|
||||||
|
-- Файл: migrations/create_city_objects_table_minimal.sql
|
||||||
|
|
||||||
|
-- Удаляем таблицу если существует (для пересоздания)
|
||||||
|
DROP TABLE IF EXISTS city_objects CASCADE;
|
||||||
|
|
||||||
|
-- Создание таблицы city_objects
|
||||||
|
CREATE TABLE city_objects (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
city_id INTEGER NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
model_url VARCHAR(500) NOT NULL,
|
||||||
|
|
||||||
|
-- Позиция
|
||||||
|
pos_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_z DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Поворот (в радианах)
|
||||||
|
rot_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_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,
|
||||||
|
|
||||||
|
-- Дополнительные свойства
|
||||||
|
organization_id INTEGER NOT NULL DEFAULT 2,
|
||||||
|
rent DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
tax DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
collidable BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
interior_id INTEGER NOT NULL DEFAULT 101,
|
||||||
|
textures VARCHAR(500) NOT NULL DEFAULT '-'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Создание индексов
|
||||||
|
CREATE INDEX idx_city_objects_city_id ON city_objects(city_id);
|
||||||
|
CREATE INDEX idx_city_objects_organization_id ON city_objects(organization_id);
|
||||||
|
CREATE INDEX idx_city_objects_interior_id ON city_objects(interior_id);
|
||||||
|
CREATE INDEX idx_city_objects_position ON city_objects(pos_x, pos_y, pos_z);
|
||||||
|
CREATE INDEX idx_city_objects_collidable ON city_objects(collidable);
|
||||||
68
migrations/create_city_objects_table_simple.sql
Normal file
68
migrations/create_city_objects_table_simple.sql
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
-- Простая миграция для создания таблицы city_objects
|
||||||
|
-- Файл: migrations/create_city_objects_table_simple.sql
|
||||||
|
|
||||||
|
-- Создание таблицы city_objects
|
||||||
|
CREATE TABLE IF NOT EXISTS city_objects (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
city_id INTEGER NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
model_url VARCHAR(500) NOT NULL,
|
||||||
|
|
||||||
|
-- Позиция
|
||||||
|
pos_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
pos_z DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- Поворот (в радианах)
|
||||||
|
rot_x DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_y DECIMAL(15, 6) NOT NULL DEFAULT 0,
|
||||||
|
rot_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,
|
||||||
|
|
||||||
|
-- Дополнительные свойства
|
||||||
|
organization_id INTEGER NOT NULL DEFAULT 2,
|
||||||
|
rent DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
tax DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||||
|
collidable BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
interior_id INTEGER NOT NULL DEFAULT 101,
|
||||||
|
textures VARCHAR(500) NOT NULL DEFAULT '-',
|
||||||
|
|
||||||
|
-- Метаданные
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Создание индексов для оптимизации запросов
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_city_objects_city_id ON city_objects(city_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_city_objects_organization_id ON city_objects(organization_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_city_objects_interior_id ON city_objects(interior_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_city_objects_position ON city_objects(pos_x, pos_y, pos_z);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_city_objects_collidable ON city_objects(collidable);
|
||||||
|
|
||||||
|
-- Комментарии к таблице и колонкам
|
||||||
|
COMMENT ON TABLE city_objects IS 'Таблица объектов городов';
|
||||||
|
COMMENT ON COLUMN city_objects.id IS 'Уникальный идентификатор объекта';
|
||||||
|
COMMENT ON COLUMN city_objects.city_id IS 'ID города, к которому принадлежит объект';
|
||||||
|
COMMENT ON COLUMN city_objects.name IS 'Название объекта';
|
||||||
|
COMMENT ON COLUMN city_objects.model_url IS 'URL модели объекта';
|
||||||
|
COMMENT ON COLUMN city_objects.pos_x IS 'X координата позиции';
|
||||||
|
COMMENT ON COLUMN city_objects.pos_y IS 'Y координата позиции';
|
||||||
|
COMMENT ON COLUMN city_objects.pos_z IS 'Z координата позиции';
|
||||||
|
COMMENT ON COLUMN city_objects.rot_x IS 'X компонент поворота (радианы)';
|
||||||
|
COMMENT ON COLUMN city_objects.rot_y IS 'Y компонент поворота (радианы)';
|
||||||
|
COMMENT ON COLUMN city_objects.rot_z IS 'Z компонент поворота (радианы)';
|
||||||
|
COMMENT ON COLUMN city_objects.scale_x IS 'X компонент масштаба';
|
||||||
|
COMMENT ON COLUMN city_objects.scale_y IS 'Y компонент масштаба';
|
||||||
|
COMMENT ON COLUMN city_objects.scale_z IS 'Z компонент масштаба';
|
||||||
|
COMMENT ON COLUMN city_objects.organization_id IS 'ID организации-владельца';
|
||||||
|
COMMENT ON COLUMN city_objects.rent IS 'Стоимость аренды';
|
||||||
|
COMMENT ON COLUMN city_objects.tax IS 'Налог';
|
||||||
|
COMMENT ON COLUMN city_objects.collidable IS 'Имеет ли объект коллизию';
|
||||||
|
COMMENT ON COLUMN city_objects.interior_id IS 'ID интерьера объекта';
|
||||||
|
COMMENT ON COLUMN city_objects.textures IS 'Информация о текстурах';
|
||||||
|
COMMENT ON COLUMN city_objects.created_at IS 'Время создания записи';
|
||||||
|
COMMENT ON COLUMN city_objects.updated_at IS 'Время последнего обновления записи';
|
||||||
79
migrations/create_colliders_table.sql
Normal file
79
migrations/create_colliders_table.sql
Normal 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 'Время последнего обновления записи';
|
||||||
@@ -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 [32m++[m[31m--[m
|
|
||||||
db.js | 4 [32m+[m[31m---[m
|
|
||||||
db1.js | 2 [32m+[m[31m-[m
|
|
||||||
package-lock.json | 57 [32m+++++++++++++++++++++++++++++[m[31m--------------------------[m
|
|
||||||
server.js | 10 [32m++++[m[31m------[m
|
|
||||||
src/Game.js | 40 [32m+++++++++++++[m[31m-------------------------[m
|
|
||||||
6 files changed, 51 insertions(+), 66 deletions(-)
|
|
||||||
196
package-lock.json
generated
196
package-lock.json
generated
@@ -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
3
public/colliders.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"colliders": []
|
||||||
|
}
|
||||||
6
public/colliders_city_1.json
Normal file
6
public/colliders_city_1.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"colliders": [
|
||||||
|
{
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
public/models/interiors/Apartment.glb
Normal file
BIN
public/models/interiors/Apartment.glb
Normal file
Binary file not shown.
BIN
public/models/interiors/Apartment2.glb
Normal file
BIN
public/models/interiors/Apartment2.glb
Normal file
Binary file not shown.
BIN
public/models/interiors/HouseInterior.glb
Normal file
BIN
public/models/interiors/HouseInterior.glb
Normal file
Binary file not shown.
0
rltn.online
Normal file
0
rltn.online
Normal file
50
run-city-objects-migration.js
Normal file
50
run-city-objects-migration.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скрипт для создания таблицы city_objects
|
||||||
|
* Запуск: node run-city-objects-migration.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const db = require('./db');
|
||||||
|
|
||||||
|
async function runMigration() {
|
||||||
|
try {
|
||||||
|
console.log('🚀 Запуск миграции для создания таблицы city_objects...');
|
||||||
|
|
||||||
|
// Читаем SQL файл миграции
|
||||||
|
const migrationPath = path.join(__dirname, 'migrations', 'create_city_objects_table.sql');
|
||||||
|
const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
|
||||||
|
|
||||||
|
console.log('📄 Загружен файл миграции:', migrationPath);
|
||||||
|
|
||||||
|
// Выполняем миграцию
|
||||||
|
await db.query(migrationSQL);
|
||||||
|
|
||||||
|
console.log('✅ Миграция успешно выполнена!');
|
||||||
|
console.log('📊 Таблица city_objects создана со всеми необходимыми индексами и триггерами');
|
||||||
|
|
||||||
|
// Проверяем, что таблица создалась
|
||||||
|
const { rows } = await db.query(`
|
||||||
|
SELECT table_name, column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'city_objects'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('\n📋 Структура созданной таблицы:');
|
||||||
|
console.table(rows);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при выполнении миграции:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
// Закрываем соединение с БД
|
||||||
|
await db.end();
|
||||||
|
console.log('🔌 Соединение с базой данных закрыто');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем миграцию
|
||||||
|
runMigration();
|
||||||
58
run-minimal-migration.js
Normal file
58
run-minimal-migration.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Минимальный скрипт для создания таблицы city_objects
|
||||||
|
* Запуск: node run-minimal-migration.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const db = require('./db');
|
||||||
|
|
||||||
|
async function runMigration() {
|
||||||
|
try {
|
||||||
|
console.log('🚀 Запуск минимальной миграции для создания таблицы city_objects...');
|
||||||
|
|
||||||
|
// Читаем минимальный SQL файл миграции
|
||||||
|
const migrationPath = path.join(__dirname, 'migrations', 'create_city_objects_table_minimal.sql');
|
||||||
|
const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
|
||||||
|
|
||||||
|
console.log('📄 Загружен файл миграции:', migrationPath);
|
||||||
|
|
||||||
|
// Выполняем миграцию
|
||||||
|
await db.query(migrationSQL);
|
||||||
|
|
||||||
|
console.log('✅ Минимальная миграция успешно выполнена!');
|
||||||
|
console.log('📊 Таблица city_objects создана со всеми необходимыми индексами');
|
||||||
|
|
||||||
|
// Проверяем, что таблица создалась
|
||||||
|
const { rows } = await db.query(`
|
||||||
|
SELECT table_name, column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'city_objects'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('\n📋 Структура созданной таблицы:');
|
||||||
|
console.table(rows);
|
||||||
|
|
||||||
|
// Проверяем количество записей
|
||||||
|
const { rows: countRows } = await db.query('SELECT COUNT(*) as count FROM city_objects');
|
||||||
|
console.log(`\n📈 Количество записей в таблице: ${countRows[0].count}`);
|
||||||
|
|
||||||
|
console.log('\n✅ Миграция завершена успешно!');
|
||||||
|
console.log('🎯 Теперь можно использовать редактор карт');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при выполнении миграции:', error);
|
||||||
|
console.error('💡 Попробуйте выполнить SQL вручную в pgAdmin или psql');
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
// Закрываем соединение с БД
|
||||||
|
await db.end();
|
||||||
|
console.log('🔌 Соединение с базой данных закрыто');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем миграцию
|
||||||
|
runMigration();
|
||||||
54
run-simple-migration.js
Normal file
54
run-simple-migration.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Простой скрипт для создания таблицы city_objects
|
||||||
|
* Запуск: node run-simple-migration.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const db = require('./db');
|
||||||
|
|
||||||
|
async function runMigration() {
|
||||||
|
try {
|
||||||
|
console.log('🚀 Запуск простой миграции для создания таблицы city_objects...');
|
||||||
|
|
||||||
|
// Читаем простой SQL файл миграции
|
||||||
|
const migrationPath = path.join(__dirname, 'migrations', 'create_city_objects_table_simple.sql');
|
||||||
|
const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
|
||||||
|
|
||||||
|
console.log('📄 Загружен файл миграции:', migrationPath);
|
||||||
|
|
||||||
|
// Выполняем миграцию
|
||||||
|
await db.query(migrationSQL);
|
||||||
|
|
||||||
|
console.log('✅ Простая миграция успешно выполнена!');
|
||||||
|
console.log('📊 Таблица city_objects создана со всеми необходимыми индексами');
|
||||||
|
|
||||||
|
// Проверяем, что таблица создалась
|
||||||
|
const { rows } = await db.query(`
|
||||||
|
SELECT table_name, column_name, data_type, is_nullable, column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'city_objects'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('\n📋 Структура созданной таблицы:');
|
||||||
|
console.table(rows);
|
||||||
|
|
||||||
|
// Проверяем количество записей
|
||||||
|
const { rows: countRows } = await db.query('SELECT COUNT(*) as count FROM city_objects');
|
||||||
|
console.log(`\n📈 Количество записей в таблице: ${countRows[0].count}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка при выполнении миграции:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
// Закрываем соединение с БД
|
||||||
|
await db.end();
|
||||||
|
console.log('🔌 Соединение с базой данных закрыто');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем миграцию
|
||||||
|
runMigration();
|
||||||
844
saves/city_1_1759940277773.txt
Normal file
844
saves/city_1_1759940277773.txt
Normal file
@@ -0,0 +1,844 @@
|
|||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": 108,
|
||||||
|
"name": "Apartment",
|
||||||
|
"model_url": "/models/interiors/Apartment.glb",
|
||||||
|
"pos_x": -50,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 35,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 109,
|
||||||
|
"name": "Apartment2",
|
||||||
|
"model_url": "/models/interiors/Apartment2.glb",
|
||||||
|
"pos_x": -75,
|
||||||
|
"pos_y": -98.8,
|
||||||
|
"pos_z": 40,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 63,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 87,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 58.1904579913708,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 91,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -58.97207259328343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 92,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -87.23030159511138,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 93,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -81.52825641321579,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 29.27078796426943,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 87.88552705994904,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.45858490544211,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 83,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 57.93990610643077,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 61,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -29.96357409929672,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 86,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.874824485167927,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 64,
|
||||||
|
"name": "building-policestation-garage.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation-garage.glb",
|
||||||
|
"pos_x": -27.423751970878307,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.838392995823742,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 75,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 28.241599465559343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 76,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 57.946895801311726,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -78.34112358353988,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -1.785629723018183e-12,
|
||||||
|
"rot_y": -1.570599999999441,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 89,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -1.191836754864397,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 90,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -60.119493751365155,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.57065,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 96,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 24.643206353028262,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 54.13689638921812,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 98,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 83.76919927546638,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 99,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -90.02515975957242,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 77,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -59.69463049725025,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -78.36798686600804,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 94,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 87.66787882117411,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -29.932796810647346,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -0.7859425349289495,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 28.49213398736152,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 58.34626836120699,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 84,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.86841846742372,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 88,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.32620221386091,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.56,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 100,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-intersection.glb",
|
||||||
|
"pos_x": -30.48563054716506,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": -5.6107,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 4.5914,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 7.4754,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 110,
|
||||||
|
"name": "HouseInterior",
|
||||||
|
"model_url": "/models/interiors/HouseInterior.glb",
|
||||||
|
"pos_x": -30,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 5,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": -16.821115520649347,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 103,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 38.21535483922429,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 104,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 85.0375477463486,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 66,
|
||||||
|
"name": "building-post.glb",
|
||||||
|
"model_url": "/models/copied/building-post.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 16.497853301459426,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 65,
|
||||||
|
"name": "industry-building.glb",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 68,
|
||||||
|
"name": "building-office-pyramid.glb",
|
||||||
|
"model_url": "/models/copied/building-office-pyramid.glb",
|
||||||
|
"pos_x": 19.415925844252136,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 18.376824247651257,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"name": "building-cinema.glb",
|
||||||
|
"model_url": "/models/copied/building-cinema.glb",
|
||||||
|
"pos_x": 2.8573192105015934,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -72.77167092884889,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 70,
|
||||||
|
"name": "building-policestation.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation.glb",
|
||||||
|
"pos_x": -18.607680210936962,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.868374285910367,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"name": "building-cabin-big.glb",
|
||||||
|
"model_url": "/models/copied/building-cabin-big.glb",
|
||||||
|
"pos_x": 23.403643205836772,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 7.1585742294568835,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 73,
|
||||||
|
"name": "building-office-rounded.glb",
|
||||||
|
"model_url": "/models/copied/building-office-rounded.glb",
|
||||||
|
"pos_x": -52.30932724265092,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.883709720266896,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 105,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 64.0119941694064,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -45,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -65,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -85,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"name": "building-cafe.glb",
|
||||||
|
"model_url": "/models/copied/building-cafe.glb",
|
||||||
|
"pos_x": -16.673190836454722,
|
||||||
|
"pos_y": 0.05806883345290892,
|
||||||
|
"pos_z": -71.80856311211987,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 102,
|
||||||
|
"name": "data-center.glb",
|
||||||
|
"model_url": "/models/copied/data-center.glb",
|
||||||
|
"pos_x": -49.292792634687814,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -71.29135417925033,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 106,
|
||||||
|
"name": "building-cafe_int",
|
||||||
|
"model_url": "/models/interiors/bar_scene.glb",
|
||||||
|
"pos_x": -16,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": -71,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "building-cinema.glb_copy",
|
||||||
|
"model_url": "/models/copied/building-cinema.glb",
|
||||||
|
"pos_x": 24.782835250646734,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -72.77167092884889,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"removedIds": []
|
||||||
|
}
|
||||||
870
saves/city_1_1759942986464.txt
Normal file
870
saves/city_1_1759942986464.txt
Normal file
@@ -0,0 +1,870 @@
|
|||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": 109,
|
||||||
|
"name": "Apartment2",
|
||||||
|
"model_url": "/models/interiors/Apartment2.glb",
|
||||||
|
"pos_x": -75,
|
||||||
|
"pos_y": -98.8,
|
||||||
|
"pos_z": 40,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 108,
|
||||||
|
"name": "Apartment",
|
||||||
|
"model_url": "/models/interiors/Apartment.glb",
|
||||||
|
"pos_x": -50,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 35,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 64,
|
||||||
|
"name": "building-policestation-garage.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation-garage.glb",
|
||||||
|
"pos_x": -27.423751970878307,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.838392995823742,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 63,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 87,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 58.1904579913708,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 91,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -58.97207259328343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 92,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -87.23030159511138,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 93,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -81.52825641321579,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 29.27078796426943,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 87.88552705994904,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.45858490544211,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 83,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 57.93990610643077,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 61,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -29.96357409929672,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 86,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.874824485167927,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 110,
|
||||||
|
"name": "HouseInterior",
|
||||||
|
"model_url": "/models/interiors/HouseInterior.glb",
|
||||||
|
"pos_x": -30,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 5,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 75,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 28.241599465559343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 76,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 57.946895801311726,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -78.34112358353988,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -1.785629723018183e-12,
|
||||||
|
"rot_y": -1.570599999999441,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 89,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -1.191836754864397,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 90,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -60.119493751365155,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.57065,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 96,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 24.643206353028262,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 54.13689638921812,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 98,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 83.76919927546638,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 99,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -90.02515975957242,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 77,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -59.69463049725025,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -78.36798686600804,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 94,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 87.66787882117411,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -29.932796810647346,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -0.7859425349289495,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 28.49213398736152,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 58.34626836120699,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 84,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.86841846742372,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 88,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.32620221386091,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.56,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 100,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-intersection.glb",
|
||||||
|
"pos_x": -30.48563054716506,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": -5.6107,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 4.5914,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 7.4754,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 65,
|
||||||
|
"name": "industry-building.glb",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": -15.380137446932416,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": -16.821115520649347,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 103,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 38.21535483922429,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 104,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 85.0375477463486,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 68,
|
||||||
|
"name": "building-office-pyramid.glb",
|
||||||
|
"model_url": "/models/copied/building-office-pyramid.glb",
|
||||||
|
"pos_x": 19.415925844252136,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 18.376824247651257,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 70,
|
||||||
|
"name": "building-policestation.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation.glb",
|
||||||
|
"pos_x": -18.607680210936962,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.868374285910367,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"name": "building-cinema.glb",
|
||||||
|
"model_url": "/models/copied/building-cinema.glb",
|
||||||
|
"pos_x": 2.8573192105015934,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -72.77167092884889,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"name": "building-cabin-big.glb",
|
||||||
|
"model_url": "/models/copied/building-cabin-big.glb",
|
||||||
|
"pos_x": 22.203643205836755,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 7.1585742294568835,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 105,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 64.0119941694064,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 73,
|
||||||
|
"name": "building-office-rounded.glb",
|
||||||
|
"model_url": "/models/copied/building-office-rounded.glb",
|
||||||
|
"pos_x": -52.30932724265092,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.883709720266896,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -45,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -85,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -65,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"name": "building-cafe.glb",
|
||||||
|
"model_url": "/models/copied/building-cafe.glb",
|
||||||
|
"pos_x": -16.673190836454722,
|
||||||
|
"pos_y": 0.05806883345290892,
|
||||||
|
"pos_z": -71.80856311211987,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 102,
|
||||||
|
"name": "data-center.glb",
|
||||||
|
"model_url": "/models/copied/data-center.glb",
|
||||||
|
"pos_x": -49.292792634687814,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -71.29135417925033,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 66,
|
||||||
|
"name": "building-post.glb",
|
||||||
|
"model_url": "/models/copied/building-post.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 16.497853301459426,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 106,
|
||||||
|
"name": "building-cafe_int",
|
||||||
|
"model_url": "/models/interiors/bar_scene.glb",
|
||||||
|
"pos_x": -16,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": -71,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "industry-building.glb_copy",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 10.287143598977302,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "industry-building.glb_copy",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 34.94669620868636,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "industry-building.glb_copy",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 60.8544602660265,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"removedIds": []
|
||||||
|
}
|
||||||
844
saves/city_1_1759943407224.txt
Normal file
844
saves/city_1_1759943407224.txt
Normal file
@@ -0,0 +1,844 @@
|
|||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": 108,
|
||||||
|
"name": "Apartment",
|
||||||
|
"model_url": "/models/interiors/Apartment.glb",
|
||||||
|
"pos_x": -50,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 35,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "5",
|
||||||
|
"scale_y": "5",
|
||||||
|
"scale_z": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 109,
|
||||||
|
"name": "Apartment2",
|
||||||
|
"model_url": "/models/interiors/Apartment2.glb",
|
||||||
|
"pos_x": -75,
|
||||||
|
"pos_y": -98.8,
|
||||||
|
"pos_z": 40,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "5",
|
||||||
|
"scale_y": "5",
|
||||||
|
"scale_z": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 63,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 87,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 58.1904579913708,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 91,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -58.97207259328343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 92,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -87.23030159511138,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 93,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -81.52825641321579,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 29.27078796426943,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 87.88552705994904,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.45858490544211,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 83,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 57.93990610643077,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 61,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -29.96357409929672,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 86,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.874824485167927,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 64,
|
||||||
|
"name": "building-policestation-garage.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation-garage.glb",
|
||||||
|
"pos_x": -27.423751970878307,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.838392995823742,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 110,
|
||||||
|
"name": "HouseInterior",
|
||||||
|
"model_url": "/models/interiors/HouseInterior.glb",
|
||||||
|
"pos_x": -30,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 5,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 75,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 28.241599465559343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 76,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 57.946895801311726,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -78.34112358353988,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -1.785629723018183e-12,
|
||||||
|
"rot_y": -1.570599999999441,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 89,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -1.191836754864397,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 90,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -60.119493751365155,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.57065,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 96,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 24.643206353028262,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 54.13689638921812,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 98,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 83.76919927546638,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 99,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -90.02515975957242,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 77,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -59.69463049725025,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -78.36798686600804,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 94,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 87.66787882117411,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -29.932796810647346,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -0.7859425349289495,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 28.49213398736152,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 58.34626836120699,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 84,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.86841846742372,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 88,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.32620221386091,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.56,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 100,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-intersection.glb",
|
||||||
|
"pos_x": -30.48563054716506,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": -5.6107,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 4.5914,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 7.4754,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": -16.821115520649347,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 103,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 38.21535483922429,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 104,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 85.0375477463486,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 65,
|
||||||
|
"name": "industry-building.glb",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 66,
|
||||||
|
"name": "building-post.glb",
|
||||||
|
"model_url": "/models/copied/building-post.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 16.497853301459426,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 68,
|
||||||
|
"name": "building-office-pyramid.glb",
|
||||||
|
"model_url": "/models/copied/building-office-pyramid.glb",
|
||||||
|
"pos_x": 19.415925844252136,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 18.376824247651257,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"name": "building-cinema.glb",
|
||||||
|
"model_url": "/models/copied/building-cinema.glb",
|
||||||
|
"pos_x": 2.8573192105015934,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -72.77167092884889,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 70,
|
||||||
|
"name": "building-policestation.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation.glb",
|
||||||
|
"pos_x": -18.607680210936962,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.868374285910367,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"name": "building-cabin-big.glb",
|
||||||
|
"model_url": "/models/copied/building-cabin-big.glb",
|
||||||
|
"pos_x": 22.203643205836755,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 7.1585742294568835,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 105,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 64.0119941694064,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 73,
|
||||||
|
"name": "building-office-rounded.glb",
|
||||||
|
"model_url": "/models/copied/building-office-rounded.glb",
|
||||||
|
"pos_x": -52.30932724265092,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.883709720266896,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -45,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -65,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -85,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 102,
|
||||||
|
"name": "data-center.glb",
|
||||||
|
"model_url": "/models/copied/data-center.glb",
|
||||||
|
"pos_x": -49.292792634687814,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -71.29135417925033,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"name": "building-cafe.glb",
|
||||||
|
"model_url": "/models/copied/building-cafe.glb",
|
||||||
|
"pos_x": -16.673190836454722,
|
||||||
|
"pos_y": 0.05806883345290892,
|
||||||
|
"pos_z": -71.80856311211987,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 106,
|
||||||
|
"name": "building-cafe_int",
|
||||||
|
"model_url": "/models/interiors/bar_scene.glb",
|
||||||
|
"pos_x": -16,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": -71,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "0.35",
|
||||||
|
"scale_y": "0.35",
|
||||||
|
"scale_z": "0.35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "building-house-modern-big.glb_copy",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 28.988434401953725,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"removedIds": []
|
||||||
|
}
|
||||||
845
saves/city_1_1759943545445.txt
Normal file
845
saves/city_1_1759943545445.txt
Normal file
@@ -0,0 +1,845 @@
|
|||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": 108,
|
||||||
|
"name": "Apartment",
|
||||||
|
"model_url": "/models/interiors/Apartment.glb",
|
||||||
|
"pos_x": -50,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 35,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "5",
|
||||||
|
"scale_y": "5",
|
||||||
|
"scale_z": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 109,
|
||||||
|
"name": "Apartment2",
|
||||||
|
"model_url": "/models/interiors/Apartment2.glb",
|
||||||
|
"pos_x": -75,
|
||||||
|
"pos_y": -98.8,
|
||||||
|
"pos_z": 40,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "5",
|
||||||
|
"scale_y": "5",
|
||||||
|
"scale_z": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 63,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 87,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 58.1904579913708,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 91,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -58.97207259328343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 92,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -87.23030159511138,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 93,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -81.52825641321579,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 29.27078796426943,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 87.88552705994904,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.45858490544211,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 83,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 57.93990610643077,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 61,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -29.96357409929672,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 86,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.874824485167927,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 64,
|
||||||
|
"name": "building-policestation-garage.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation-garage.glb",
|
||||||
|
"pos_x": -27.423751970878307,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.838392995823742,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 75,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 28.241599465559343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 76,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 57.946895801311726,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -78.34112358353988,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -1.785629723018183e-12,
|
||||||
|
"rot_y": -1.570599999999441,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 89,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -1.191836754864397,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 90,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -60.119493751365155,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.57065,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 96,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 24.643206353028262,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 54.13689638921812,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 98,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 83.76919927546638,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 99,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -90.02515975957242,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 77,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -59.69463049725025,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -78.36798686600804,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 94,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 87.66787882117411,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -29.932796810647346,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -0.7859425349289495,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 28.49213398736152,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 58.34626836120699,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 84,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.86841846742372,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 88,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.32620221386091,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.56,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 110,
|
||||||
|
"name": "HouseInterior",
|
||||||
|
"model_url": "/models/interiors/HouseInterior.glb",
|
||||||
|
"pos_x": -30,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 5,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 100,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-intersection.glb",
|
||||||
|
"pos_x": -30.48563054716506,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": -5.6107,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 4.5914,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 7.4754,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": -16.821115520649347,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 103,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 38.21535483922429,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 104,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 85.0375477463486,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 65,
|
||||||
|
"name": "industry-building.glb",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 66,
|
||||||
|
"name": "building-post.glb",
|
||||||
|
"model_url": "/models/copied/building-post.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 16.497853301459426,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"name": "building-cinema.glb",
|
||||||
|
"model_url": "/models/copied/building-cinema.glb",
|
||||||
|
"pos_x": 2.8573192105015934,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -72.77167092884889,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 68,
|
||||||
|
"name": "building-office-pyramid.glb",
|
||||||
|
"model_url": "/models/copied/building-office-pyramid.glb",
|
||||||
|
"pos_x": 19.415925844252136,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 18.376824247651257,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 105,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 64.0119941694064,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"name": "building-cabin-big.glb",
|
||||||
|
"model_url": "/models/copied/building-cabin-big.glb",
|
||||||
|
"pos_x": 22.203643205836755,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 70,
|
||||||
|
"name": "building-policestation.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation.glb",
|
||||||
|
"pos_x": -18.607680210936962,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.868374285910367,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 73,
|
||||||
|
"name": "building-office-rounded.glb",
|
||||||
|
"model_url": "/models/copied/building-office-rounded.glb",
|
||||||
|
"pos_x": -52.30932724265092,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.883709720266896,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 7.1585742294568835,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -65,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -85,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -45,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 102,
|
||||||
|
"name": "data-center.glb",
|
||||||
|
"model_url": "/models/copied/data-center.glb",
|
||||||
|
"pos_x": -49.292792634687814,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -71.29135417925033,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"name": "building-cafe.glb",
|
||||||
|
"model_url": "/models/copied/building-cafe.glb",
|
||||||
|
"pos_x": -16.673190836454722,
|
||||||
|
"pos_y": 0.05806883345290892,
|
||||||
|
"pos_z": -71.80856311211987,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 106,
|
||||||
|
"name": "building-cafe_int",
|
||||||
|
"model_url": "/models/interiors/bar_scene.glb",
|
||||||
|
"pos_x": -16,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": -71,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "0.35",
|
||||||
|
"scale_y": "0.35",
|
||||||
|
"scale_z": "0.35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 107,
|
||||||
|
"name": "bench-forest.glb",
|
||||||
|
"model_url": "/models/copied/bench-forest.glb",
|
||||||
|
"pos_x": 46.30092962214616,
|
||||||
|
"pos_y": -5.678365161053161,
|
||||||
|
"pos_z": 16.66887032489769,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"removedIds": []
|
||||||
|
}
|
||||||
858
saves/city_1_1759943997101.txt
Normal file
858
saves/city_1_1759943997101.txt
Normal file
@@ -0,0 +1,858 @@
|
|||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": 109,
|
||||||
|
"name": "Apartment2",
|
||||||
|
"model_url": "/models/interiors/Apartment2.glb",
|
||||||
|
"pos_x": -75,
|
||||||
|
"pos_y": -98.8,
|
||||||
|
"pos_z": 40,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "5",
|
||||||
|
"scale_y": "5",
|
||||||
|
"scale_z": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 107,
|
||||||
|
"name": "bench-forest.glb",
|
||||||
|
"model_url": "/models/copied/bench-forest.glb",
|
||||||
|
"pos_x": 90.1649895833779,
|
||||||
|
"pos_y": 90.16498958337793,
|
||||||
|
"pos_z": 90.16498958337792,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 108,
|
||||||
|
"name": "Apartment",
|
||||||
|
"model_url": "/models/interiors/Apartment.glb",
|
||||||
|
"pos_x": -50,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 35,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "5",
|
||||||
|
"scale_y": "5",
|
||||||
|
"scale_z": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 63,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 87,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 58.1904579913708,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 91,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -58.97207259328343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 92,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -87.23030159511138,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 93,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -81.52825641321579,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 29.27078796426943,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 87.88552705994904,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.45858490544211,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 83,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 57.93990610643077,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 61,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -29.96357409929672,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 86,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.874824485167927,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 64,
|
||||||
|
"name": "building-policestation-garage.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation-garage.glb",
|
||||||
|
"pos_x": -27.423751970878307,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.838392995823742,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 75,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 28.241599465559343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 76,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 57.946895801311726,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -78.34112358353988,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -1.785629723018183e-12,
|
||||||
|
"rot_y": -1.570599999999441,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 89,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -1.191836754864397,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 90,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -60.119493751365155,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.57065,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 96,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 24.643206353028262,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 54.13689638921812,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 98,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 83.76919927546638,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 99,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -90.02515975957242,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 77,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -59.69463049725025,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -78.36798686600804,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 94,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 87.66787882117411,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -29.932796810647346,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -0.7859425349289495,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 28.49213398736152,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 58.34626836120699,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 84,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.86841846742372,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 88,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.32620221386091,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.56,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 100,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-intersection.glb",
|
||||||
|
"pos_x": -30.48563054716506,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": -5.6107,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 4.5914,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 7.4754,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 110,
|
||||||
|
"name": "HouseInterior",
|
||||||
|
"model_url": "/models/interiors/HouseInterior.glb",
|
||||||
|
"pos_x": -30,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 5,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 103,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 38.21535483922429,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": -16.821115520649347,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 104,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 85.0375477463486,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 66,
|
||||||
|
"name": "building-post.glb",
|
||||||
|
"model_url": "/models/copied/building-post.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 16.497853301459426,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 65,
|
||||||
|
"name": "industry-building.glb",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 68,
|
||||||
|
"name": "building-office-pyramid.glb",
|
||||||
|
"model_url": "/models/copied/building-office-pyramid.glb",
|
||||||
|
"pos_x": 19.415925844252136,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 18.376824247651257,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"name": "building-cinema.glb",
|
||||||
|
"model_url": "/models/copied/building-cinema.glb",
|
||||||
|
"pos_x": 2.8573192105015934,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -72.77167092884889,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 70,
|
||||||
|
"name": "building-policestation.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation.glb",
|
||||||
|
"pos_x": -18.607680210936962,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.868374285910367,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"name": "building-cabin-big.glb",
|
||||||
|
"model_url": "/models/copied/building-cabin-big.glb",
|
||||||
|
"pos_x": 22.203643205836755,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 7.1585742294568835,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 105,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 64.0119941694064,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 73,
|
||||||
|
"name": "building-office-rounded.glb",
|
||||||
|
"model_url": "/models/copied/building-office-rounded.glb",
|
||||||
|
"pos_x": -52.30932724265092,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.883709720266896,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -45,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -65,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -85,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"name": "building-cafe.glb",
|
||||||
|
"model_url": "/models/copied/building-cafe.glb",
|
||||||
|
"pos_x": -16.673190836454722,
|
||||||
|
"pos_y": 0.05806883345290892,
|
||||||
|
"pos_z": -71.80856311211987,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 102,
|
||||||
|
"name": "data-center.glb",
|
||||||
|
"model_url": "/models/copied/data-center.glb",
|
||||||
|
"pos_x": -49.292792634687814,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -71.29135417925033,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 106,
|
||||||
|
"name": "building-cafe_int",
|
||||||
|
"model_url": "/models/interiors/bar_scene.glb",
|
||||||
|
"pos_x": -16,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": -71,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "0.35",
|
||||||
|
"scale_y": "0.35",
|
||||||
|
"scale_z": "0.35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "industry-building.glb_copy",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 28.23817863777715,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"removedIds": []
|
||||||
|
}
|
||||||
858
saves/city_1_1759944381024.txt
Normal file
858
saves/city_1_1759944381024.txt
Normal file
@@ -0,0 +1,858 @@
|
|||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": 107,
|
||||||
|
"name": "bench-forest.glb",
|
||||||
|
"model_url": "/models/copied/bench-forest.glb",
|
||||||
|
"pos_x": 90.1649895833779,
|
||||||
|
"pos_y": 90.16498958337793,
|
||||||
|
"pos_z": 90.16498958337792,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 108,
|
||||||
|
"name": "Apartment",
|
||||||
|
"model_url": "/models/interiors/Apartment.glb",
|
||||||
|
"pos_x": -50,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 35,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "5",
|
||||||
|
"scale_y": "5",
|
||||||
|
"scale_z": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 109,
|
||||||
|
"name": "Apartment2",
|
||||||
|
"model_url": "/models/interiors/Apartment2.glb",
|
||||||
|
"pos_x": -75,
|
||||||
|
"pos_y": -98.8,
|
||||||
|
"pos_z": 40,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "5",
|
||||||
|
"scale_y": "5",
|
||||||
|
"scale_z": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 63,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 87,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 58.1904579913708,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 91,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -58.97207259328343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 92,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -87.23030159511138,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 93,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -81.52825641321579,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 29.27078796426943,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 87.88552705994904,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.45858490544211,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 83,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 57.93990610643077,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 61,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -29.96357409929672,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 86,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.874824485167927,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 64,
|
||||||
|
"name": "building-policestation-garage.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation-garage.glb",
|
||||||
|
"pos_x": -27.423751970878307,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.838392995823742,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 75,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 28.241599465559343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 76,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 57.946895801311726,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -78.34112358353988,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -1.785629723018183e-12,
|
||||||
|
"rot_y": -1.570599999999441,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 89,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -1.191836754864397,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 90,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -60.119493751365155,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.57065,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 96,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 24.643206353028262,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 54.13689638921812,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 98,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 83.76919927546638,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 99,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -90.02515975957242,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 77,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -59.69463049725025,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -78.36798686600804,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 94,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 87.66787882117411,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -29.932796810647346,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -0.7859425349289495,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 28.49213398736152,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 58.34626836120699,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 84,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.86841846742372,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 88,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.32620221386091,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.56,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 100,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-intersection.glb",
|
||||||
|
"pos_x": -30.48563054716506,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": -5.6107,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 4.5914,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 7.4754,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 110,
|
||||||
|
"name": "HouseInterior",
|
||||||
|
"model_url": "/models/interiors/HouseInterior.glb",
|
||||||
|
"pos_x": -30,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 5,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 104,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 85.0375477463486,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 66,
|
||||||
|
"name": "building-post.glb",
|
||||||
|
"model_url": "/models/copied/building-post.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 16.497853301459426,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 68,
|
||||||
|
"name": "building-office-pyramid.glb",
|
||||||
|
"model_url": "/models/copied/building-office-pyramid.glb",
|
||||||
|
"pos_x": 19.415925844252136,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 18.376824247651257,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"name": "building-cinema.glb",
|
||||||
|
"model_url": "/models/copied/building-cinema.glb",
|
||||||
|
"pos_x": 2.8573192105015934,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -72.77167092884889,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 70,
|
||||||
|
"name": "building-policestation.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation.glb",
|
||||||
|
"pos_x": -18.607680210936962,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.868374285910367,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"name": "building-cabin-big.glb",
|
||||||
|
"model_url": "/models/copied/building-cabin-big.glb",
|
||||||
|
"pos_x": 22.203643205836755,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 7.1585742294568835,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 105,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 64.0119941694064,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 65,
|
||||||
|
"name": "industry-building.glb",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": -6.924177538527228,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 73,
|
||||||
|
"name": "building-office-rounded.glb",
|
||||||
|
"model_url": "/models/copied/building-office-rounded.glb",
|
||||||
|
"pos_x": -52.30932724265092,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.883709720266896,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": -16.821115520649347,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -45,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"name": "building-cafe.glb",
|
||||||
|
"model_url": "/models/copied/building-cafe.glb",
|
||||||
|
"pos_x": -16.673190836454722,
|
||||||
|
"pos_y": 0.05806883345290892,
|
||||||
|
"pos_z": -71.80856311211987,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -85,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 102,
|
||||||
|
"name": "data-center.glb",
|
||||||
|
"model_url": "/models/copied/data-center.glb",
|
||||||
|
"pos_x": -49.292792634687814,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -71.29135417925033,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -65,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 103,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 38.21535483922429,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 106,
|
||||||
|
"name": "building-cafe_int",
|
||||||
|
"model_url": "/models/interiors/bar_scene.glb",
|
||||||
|
"pos_x": -16,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": -71,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "0.35",
|
||||||
|
"scale_y": "0.35",
|
||||||
|
"scale_z": "0.35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "industry-building.glb_copy",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 21.049163021017154,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"removedIds": []
|
||||||
|
}
|
||||||
858
saves/city_1_1759944600793.txt
Normal file
858
saves/city_1_1759944600793.txt
Normal file
@@ -0,0 +1,858 @@
|
|||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"id": 107,
|
||||||
|
"name": "bench-forest.glb",
|
||||||
|
"model_url": "/models/copied/bench-forest.glb",
|
||||||
|
"pos_x": 90.1649895833779,
|
||||||
|
"pos_y": 90.16498958337793,
|
||||||
|
"pos_z": 90.16498958337792,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 63,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 87,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 58.1904579913708,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 91,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -58.97207259328343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 92,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -87.23030159511138,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 93,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -81.52825641321579,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 29.27078796426943,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 87.88552705994904,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.45858490544211,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 83,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 57.93990610643077,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 61,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -0.6527787912290837,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": -29.96357409929672,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 28.600685124690344,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 86,
|
||||||
|
"name": "tile-plain_grass.glb",
|
||||||
|
"model_url": "/models/copied/tile-plain_grass.glb",
|
||||||
|
"pos_x": 28.874824485167927,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.424927439019825,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 108,
|
||||||
|
"name": "Apartment",
|
||||||
|
"model_url": "/models/interiors/Apartment.glb",
|
||||||
|
"pos_x": -50,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 35,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "5",
|
||||||
|
"scale_y": "5",
|
||||||
|
"scale_z": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 109,
|
||||||
|
"name": "Apartment2",
|
||||||
|
"model_url": "/models/interiors/Apartment2.glb",
|
||||||
|
"pos_x": -75,
|
||||||
|
"pos_y": -98.8,
|
||||||
|
"pos_z": 40,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "5",
|
||||||
|
"scale_y": "5",
|
||||||
|
"scale_z": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 110,
|
||||||
|
"name": "HouseInterior",
|
||||||
|
"model_url": "/models/interiors/HouseInterior.glb",
|
||||||
|
"pos_x": -30,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": 5,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 64,
|
||||||
|
"name": "building-policestation-garage.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation-garage.glb",
|
||||||
|
"pos_x": -27.423751970878307,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.838392995823742,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 75,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 28.241599465559343,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 76,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 57.946895801311726,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -78.34112358353988,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": -1.785629723018183e-12,
|
||||||
|
"rot_y": -1.570599999999441,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 89,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -1.191836754864397,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 90,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -60.119493751365155,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.57065,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"name": "tile-mainroad-straight",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -53.022684089320066,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 96,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 24.643206353028262,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 54.13689638921812,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 98,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": 83.76919927546638,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 99,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-mainroad-straight.glb",
|
||||||
|
"pos_x": -90.02515975957242,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.23864067432326,
|
||||||
|
"rot_x": -9.919275836140717e-13,
|
||||||
|
"rot_y": -1.570649999999569,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 77,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -59.69463049725025,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -78.36798686600804,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 94,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 87.66787882117411,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -29.932796810647346,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706000000000067,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -0.7859425349289495,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 28.49213398736152,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": 58.34626836120699,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 84,
|
||||||
|
"name": "tile-road-straight.glb",
|
||||||
|
"model_url": "/models/copied/tile-road-straight.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -58.86841846742372,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 88,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.43502101608746,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -88.32620221386091,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-mainroad-intersection-t.glb",
|
||||||
|
"pos_x": -30.56,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 100,
|
||||||
|
"name": "road",
|
||||||
|
"model_url": "/models/copied/tile-road-intersection.glb",
|
||||||
|
"pos_x": -30.48563054716506,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -30.0096,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": -1.5706,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 0,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": -5.6107,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 4.5914,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -12,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"name": "tree-beech.glb",
|
||||||
|
"model_url": "/models/copied/tree-beech.glb",
|
||||||
|
"pos_x": 7.4754,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -9.9452,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": -16.821115520649347,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 103,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 38.21535483922429,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 104,
|
||||||
|
"name": "building-house-modern-big.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern-big.glb",
|
||||||
|
"pos_x": 85.0375477463486,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 65,
|
||||||
|
"name": "industry-building.glb",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 1.2272702742845656,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": -41.9436860772972,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 66,
|
||||||
|
"name": "building-post.glb",
|
||||||
|
"model_url": "/models/copied/building-post.glb",
|
||||||
|
"pos_x": -0.6583,
|
||||||
|
"pos_y": 0.22578,
|
||||||
|
"pos_z": 16.497853301459426,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 68,
|
||||||
|
"name": "building-office-pyramid.glb",
|
||||||
|
"model_url": "/models/copied/building-office-pyramid.glb",
|
||||||
|
"pos_x": 19.415925844252136,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 18.376824247651257,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"name": "building-cinema.glb",
|
||||||
|
"model_url": "/models/copied/building-cinema.glb",
|
||||||
|
"pos_x": 2.8573192105015934,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -72.77167092884889,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"name": "building-cabin-big.glb",
|
||||||
|
"model_url": "/models/copied/building-cabin-big.glb",
|
||||||
|
"pos_x": 22.203643205836755,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 70,
|
||||||
|
"name": "building-policestation.glb",
|
||||||
|
"model_url": "/models/copied/building-policestation.glb",
|
||||||
|
"pos_x": -18.607680210936962,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.868374285910367,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 7.1585742294568835,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 105,
|
||||||
|
"name": "building-house-modern.glb",
|
||||||
|
"model_url": "/models/copied/building-house-modern.glb",
|
||||||
|
"pos_x": 64.0119941694064,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -18.561659223223405,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 73,
|
||||||
|
"name": "building-office-rounded.glb",
|
||||||
|
"model_url": "/models/copied/building-office-rounded.glb",
|
||||||
|
"pos_x": -52.30932724265092,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": 15.883709720266896,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -45,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -65,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"name": "building-block-5floor.glb",
|
||||||
|
"model_url": "/models/copied/building-block-5floor.glb",
|
||||||
|
"pos_x": -85,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -16.6395979611043,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 102,
|
||||||
|
"name": "data-center.glb",
|
||||||
|
"model_url": "/models/copied/data-center.glb",
|
||||||
|
"pos_x": -49.292792634687814,
|
||||||
|
"pos_y": 0,
|
||||||
|
"pos_z": -71.29135417925033,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"name": "building-cafe.glb",
|
||||||
|
"model_url": "/models/copied/building-cafe.glb",
|
||||||
|
"pos_x": -16.673190836454722,
|
||||||
|
"pos_y": 0.05806883345290892,
|
||||||
|
"pos_z": -71.80856311211987,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "1",
|
||||||
|
"scale_y": "1",
|
||||||
|
"scale_z": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 106,
|
||||||
|
"name": "building-cafe_int",
|
||||||
|
"model_url": "/models/interiors/bar_scene.glb",
|
||||||
|
"pos_x": -16,
|
||||||
|
"pos_y": -100,
|
||||||
|
"pos_z": -71,
|
||||||
|
"rot_x": 3.1415,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 3.1415,
|
||||||
|
"scale_x": "0.35",
|
||||||
|
"scale_y": "0.35",
|
||||||
|
"scale_z": "0.35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "industry-building.glb_copy",
|
||||||
|
"model_url": "/models/copied/industry-building.glb",
|
||||||
|
"pos_x": 33.624,
|
||||||
|
"pos_y": 0.226,
|
||||||
|
"pos_z": 0,
|
||||||
|
"rot_x": 0,
|
||||||
|
"rot_y": 0,
|
||||||
|
"rot_z": 0,
|
||||||
|
"scale_x": 1,
|
||||||
|
"scale_y": 1,
|
||||||
|
"scale_z": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"removedIds": []
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"time":"2025-04-20T19:17:09.736Z","lastReal":1756219619275}
|
{"time":"2026-04-07T06:38:34.392Z","lastReal":1760015529857}
|
||||||
258
server/README.md
Normal file
258
server/README.md
Normal 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 в репозитории
|
||||||
@@ -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 });
|
||||||
|
|||||||
37
src/App.js
37
src/App.js
@@ -10,6 +10,9 @@ 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';
|
||||||
|
import ModelGallery from './pages/ModelGallery';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [isAuth, setIsAuth] = useState(!!localStorage.getItem('token'));
|
const [isAuth, setIsAuth] = useState(!!localStorage.getItem('token'));
|
||||||
@@ -77,6 +80,40 @@ 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="/model-gallery"
|
||||||
|
element={
|
||||||
|
isAuth
|
||||||
|
? <RequireProfile>
|
||||||
|
<ModelGallery />
|
||||||
|
</RequireProfile>
|
||||||
|
: <Navigate to="/login" replace/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* всё остальное */}
|
{/* всё остальное */}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
11742
src/Game.js
11742
src/Game.js
File diff suppressed because it is too large
Load Diff
611
src/api/README.md
Normal file
611
src/api/README.md
Normal 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -33,4 +33,44 @@ 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -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_')) {
|
||||||
|
|||||||
185
src/components/ModelPreview.jsx
Normal file
185
src/components/ModelPreview.jsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||||
|
|
||||||
|
const ModelPreview = ({ modelUrl, modelName, onLoad }) => {
|
||||||
|
const mountRef = useRef(null);
|
||||||
|
const sceneRef = useRef();
|
||||||
|
const rendererRef = useRef();
|
||||||
|
const cameraRef = useRef();
|
||||||
|
const controlsRef = useRef();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mountRef.current) return;
|
||||||
|
|
||||||
|
// Создаем сцену
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0xf0f0f0);
|
||||||
|
sceneRef.current = scene;
|
||||||
|
|
||||||
|
// Создаем камеру
|
||||||
|
const camera = new THREE.PerspectiveCamera(
|
||||||
|
50,
|
||||||
|
mountRef.current.clientWidth / mountRef.current.clientHeight,
|
||||||
|
0.1,
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
camera.position.set(5, 5, 5);
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
cameraRef.current = camera;
|
||||||
|
|
||||||
|
// Создаем рендерер
|
||||||
|
const renderer = new THREE.WebGLRenderer({
|
||||||
|
antialias: true,
|
||||||
|
alpha: true,
|
||||||
|
preserveDrawingBuffer: true
|
||||||
|
});
|
||||||
|
renderer.setSize(mountRef.current.clientWidth, mountRef.current.clientHeight);
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
mountRef.current.appendChild(renderer.domElement);
|
||||||
|
rendererRef.current = renderer;
|
||||||
|
|
||||||
|
// Создаем контролы
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.05;
|
||||||
|
controls.enableZoom = true;
|
||||||
|
controls.enablePan = false;
|
||||||
|
controlsRef.current = controls;
|
||||||
|
|
||||||
|
// Освещение
|
||||||
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
directionalLight.position.set(10, 10, 5);
|
||||||
|
directionalLight.castShadow = true;
|
||||||
|
directionalLight.shadow.mapSize.width = 2048;
|
||||||
|
directionalLight.shadow.mapSize.height = 2048;
|
||||||
|
scene.add(directionalLight);
|
||||||
|
|
||||||
|
// Загружаем модель
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
console.log('🔄 Загружаем модель:', modelUrl);
|
||||||
|
loader.load(
|
||||||
|
modelUrl,
|
||||||
|
(gltf) => {
|
||||||
|
console.log('✅ Модель загружена:', gltf);
|
||||||
|
const model = gltf.scene;
|
||||||
|
|
||||||
|
// Центрируем модель
|
||||||
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
const scale = 2 / maxDim;
|
||||||
|
|
||||||
|
model.scale.setScalar(scale);
|
||||||
|
model.position.sub(center.multiplyScalar(scale));
|
||||||
|
|
||||||
|
// Настраиваем материалы
|
||||||
|
model.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.castShadow = true;
|
||||||
|
child.receiveShadow = true;
|
||||||
|
|
||||||
|
// Улучшаем материалы
|
||||||
|
if (child.material) {
|
||||||
|
if (Array.isArray(child.material)) {
|
||||||
|
child.material.forEach(mat => {
|
||||||
|
if (mat.map) mat.map.anisotropy = 4;
|
||||||
|
mat.needsUpdate = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (child.material.map) child.material.map.anisotropy = 4;
|
||||||
|
child.material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.add(model);
|
||||||
|
setIsLoading(false);
|
||||||
|
if (onLoad) onLoad(model);
|
||||||
|
},
|
||||||
|
(progress) => {
|
||||||
|
console.log('Загрузка модели:', (progress.loaded / progress.total * 100) + '%');
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('❌ Ошибка загрузки модели:', error);
|
||||||
|
console.error('URL модели:', modelUrl);
|
||||||
|
setError(`Ошибка загрузки: ${error.message || 'Неизвестная ошибка'}`);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Анимация
|
||||||
|
const animate = () => {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
controls.update();
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
};
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Обработка изменения размера
|
||||||
|
const handleResize = () => {
|
||||||
|
if (!mountRef.current) return;
|
||||||
|
const width = mountRef.current.clientWidth;
|
||||||
|
const height = mountRef.current.clientHeight;
|
||||||
|
|
||||||
|
camera.aspect = width / height;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
if (mountRef.current && renderer.domElement) {
|
||||||
|
mountRef.current.removeChild(renderer.domElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [modelUrl, onLoad]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', width: '100%', height: '300px', border: '1px solid #ddd', borderRadius: '8px', overflow: 'hidden' }}>
|
||||||
|
<div ref={mountRef} style={{ width: '100%', height: '100%' }} />
|
||||||
|
{isLoading && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
background: 'rgba(255,255,255,0.9)',
|
||||||
|
padding: '10px 20px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}>
|
||||||
|
Загрузка...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
background: 'rgba(255,0,0,0.1)',
|
||||||
|
padding: '10px 20px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: 'red'
|
||||||
|
}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelPreview;
|
||||||
216
src/components/ModelThumbnail.jsx
Normal file
216
src/components/ModelThumbnail.jsx
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||||
|
|
||||||
|
const ModelThumbnail = ({ modelUrl, modelName, onImageGenerated }) => {
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [imageDataUrl, setImageDataUrl] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!modelUrl) return;
|
||||||
|
|
||||||
|
const generateThumbnail = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Создаем сцену
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0xf0f0f0);
|
||||||
|
|
||||||
|
// Создаем камеру
|
||||||
|
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 1000);
|
||||||
|
camera.position.set(5, 5, 5);
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
|
// Создаем рендерер
|
||||||
|
const renderer = new THREE.WebGLRenderer({
|
||||||
|
antialias: true,
|
||||||
|
alpha: true,
|
||||||
|
preserveDrawingBuffer: true
|
||||||
|
});
|
||||||
|
renderer.setSize(300, 300);
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
|
||||||
|
// Освещение
|
||||||
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
directionalLight.position.set(10, 10, 5);
|
||||||
|
directionalLight.castShadow = true;
|
||||||
|
scene.add(directionalLight);
|
||||||
|
|
||||||
|
// Загружаем модель
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
console.log('🔄 Генерируем превью для:', modelUrl);
|
||||||
|
|
||||||
|
const gltf = await new Promise((resolve, reject) => {
|
||||||
|
loader.load(
|
||||||
|
modelUrl,
|
||||||
|
resolve,
|
||||||
|
(progress) => {
|
||||||
|
console.log('Загрузка:', (progress.loaded / progress.total * 100) + '%');
|
||||||
|
},
|
||||||
|
reject
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Модель загружена для превью:', gltf);
|
||||||
|
const model = gltf.scene;
|
||||||
|
|
||||||
|
// Центрируем модель
|
||||||
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
const scale = 2 / maxDim;
|
||||||
|
|
||||||
|
model.scale.setScalar(scale);
|
||||||
|
model.position.sub(center.multiplyScalar(scale));
|
||||||
|
|
||||||
|
// Настраиваем материалы
|
||||||
|
model.traverse((child) => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.castShadow = true;
|
||||||
|
child.receiveShadow = true;
|
||||||
|
|
||||||
|
if (child.material) {
|
||||||
|
if (Array.isArray(child.material)) {
|
||||||
|
child.material.forEach(mat => {
|
||||||
|
if (mat.map) mat.map.anisotropy = 4;
|
||||||
|
mat.needsUpdate = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (child.material.map) child.material.map.anisotropy = 4;
|
||||||
|
child.material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.add(model);
|
||||||
|
|
||||||
|
// Рендерим сцену
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
|
||||||
|
// Получаем изображение
|
||||||
|
const canvas = renderer.domElement;
|
||||||
|
const dataUrl = canvas.toDataURL('image/png');
|
||||||
|
setImageDataUrl(dataUrl);
|
||||||
|
|
||||||
|
// Сохраняем изображение в localStorage для кэширования
|
||||||
|
const cacheKey = `model_thumb_${modelName}`;
|
||||||
|
localStorage.setItem(cacheKey, dataUrl);
|
||||||
|
|
||||||
|
if (onImageGenerated) {
|
||||||
|
onImageGenerated(dataUrl, modelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Очистка
|
||||||
|
renderer.dispose();
|
||||||
|
scene.clear();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка генерации превью:', error);
|
||||||
|
setError(`Ошибка: ${error.message || 'Неизвестная ошибка'}`);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Проверяем кэш
|
||||||
|
const cacheKey = `model_thumb_${modelName}`;
|
||||||
|
const cachedImage = localStorage.getItem(cacheKey);
|
||||||
|
|
||||||
|
if (cachedImage) {
|
||||||
|
setImageDataUrl(cachedImage);
|
||||||
|
setIsLoading(false);
|
||||||
|
if (onImageGenerated) {
|
||||||
|
onImageGenerated(cachedImage, modelName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generateThumbnail();
|
||||||
|
}
|
||||||
|
}, [modelUrl, modelName, onImageGenerated]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '300px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: '#f8f9fa',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: '8px'
|
||||||
|
}}>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '14px', marginBottom: '10px' }}>Генерация превью...</div>
|
||||||
|
<div style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
border: '3px solid #f3f3f3',
|
||||||
|
borderTop: '3px solid #007bff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite',
|
||||||
|
margin: '0 auto'
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '300px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: '#fff5f5',
|
||||||
|
border: '1px solid #fed7d7',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: '#c53030'
|
||||||
|
}}>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '14px' }}>❌ {error}</div>
|
||||||
|
<div style={{ fontSize: '12px', marginTop: '5px' }}>Файл: {modelName}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '300px',
|
||||||
|
background: '#f8f9fa',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: '8px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative'
|
||||||
|
}}>
|
||||||
|
{imageDataUrl && (
|
||||||
|
<img
|
||||||
|
src={imageDataUrl}
|
||||||
|
alt={modelName}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
background: '#f0f0f0'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelThumbnail;
|
||||||
340
src/components/README.md
Normal file
340
src/components/README.md
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
279
src/modules/CameraManager.js
Normal file
279
src/modules/CameraManager.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
300
src/modules/CollisionManager.js
Normal file
300
src/modules/CollisionManager.js
Normal 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
451
src/modules/GameCore.js
Normal 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 очищен');
|
||||||
|
}
|
||||||
|
}
|
||||||
358
src/modules/InteriorManager.js
Normal file
358
src/modules/InteriorManager.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/modules/PlayerManager.js
Normal file
163
src/modules/PlayerManager.js
Normal 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
90
src/modules/README.md
Normal 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)
|
||||||
132
src/modules/RendererManager.js
Normal file
132
src/modules/RendererManager.js
Normal 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
212
src/modules/SceneManager.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
419
src/pages/CollisionEditor.jsx
Normal file
419
src/pages/CollisionEditor.jsx
Normal 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>Город: </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>Форма: </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>
|
||||||
|
);
|
||||||
|
}
|
||||||
1652
src/pages/EnhancedCollisionEditor.jsx
Normal file
1652
src/pages/EnhancedCollisionEditor.jsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,19 @@ export default function MapEditor() {
|
|||||||
const [mode, setMode] = useState('translate');
|
const [mode, setMode] = useState('translate');
|
||||||
const [cities, setCities] = useState([]);
|
const [cities, setCities] = useState([]);
|
||||||
const [cityId, setCityId] = useState(null);
|
const [cityId, setCityId] = useState(null);
|
||||||
|
const [copiedObject, setCopiedObject] = useState(null);
|
||||||
|
const [objectCoordinates, setObjectCoordinates] = useState({ x: 0, y: 0, z: 0 });
|
||||||
|
const [objectRotation, setObjectRotation] = useState({ x: 0, y: 0, z: 0 });
|
||||||
|
const [objectScale, setObjectScale] = useState({ x: 1, y: 1, z: 1 });
|
||||||
|
const [objectId, setObjectId] = useState(null);
|
||||||
|
const [objectName, setObjectName] = useState('');
|
||||||
|
const [objectOrganizationId, setObjectOrganizationId] = useState(2);
|
||||||
|
const [objectCollidable, setObjectCollidable] = useState(false);
|
||||||
|
const [objectInteriorId, setObjectInteriorId] = useState(101);
|
||||||
|
const [showAdvancedProperties, setShowAdvancedProperties] = useState(false);
|
||||||
|
const [rulerMode, setRulerMode] = useState(false);
|
||||||
|
const [rulerPoints, setRulerPoints] = useState([]);
|
||||||
|
const keysPressed = useRef({});
|
||||||
|
|
||||||
const sceneRef = useRef();
|
const sceneRef = useRef();
|
||||||
const cameraRef = useRef();
|
const cameraRef = useRef();
|
||||||
@@ -82,12 +95,82 @@ export default function MapEditor() {
|
|||||||
}
|
}
|
||||||
transform.attach(obj);
|
transform.attach(obj);
|
||||||
selectedRef.current = obj;
|
selectedRef.current = obj;
|
||||||
|
updateObjectCoordinates(obj);
|
||||||
} else {
|
} else {
|
||||||
selectedRef.current = null;
|
selectedRef.current = null;
|
||||||
transform.detach();
|
transform.detach();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
renderer.domElement.addEventListener('pointerdown', onPointerDown);
|
renderer.domElement.addEventListener('pointerdown', onPointerDown);
|
||||||
|
|
||||||
|
// Обработчик для линейки
|
||||||
|
const onRulerClick = (event) => {
|
||||||
|
if (rulerMode) {
|
||||||
|
handleRulerClick(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
renderer.domElement.addEventListener('click', onRulerClick);
|
||||||
|
|
||||||
|
// Обработчики клавиатуры
|
||||||
|
const cameraSpeed = 0.5;
|
||||||
|
const objectMoveSpeed = 0.5;
|
||||||
|
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
keysPressed.current[event.code] = true;
|
||||||
|
|
||||||
|
// Управление камерой
|
||||||
|
if (event.code === 'KeyW' || event.code === 'KeyЦ') {
|
||||||
|
const direction = new THREE.Vector3();
|
||||||
|
camera.getWorldDirection(direction);
|
||||||
|
camera.position.add(direction.multiplyScalar(cameraSpeed));
|
||||||
|
}
|
||||||
|
if (event.code === 'KeyS' || event.code === 'KeyЫ') {
|
||||||
|
const direction = new THREE.Vector3();
|
||||||
|
camera.getWorldDirection(direction);
|
||||||
|
camera.position.sub(direction.multiplyScalar(cameraSpeed));
|
||||||
|
}
|
||||||
|
if (event.code === 'KeyA' || event.code === 'KeyФ') {
|
||||||
|
const right = new THREE.Vector3();
|
||||||
|
camera.getWorldDirection(right);
|
||||||
|
right.cross(camera.up).normalize();
|
||||||
|
camera.position.sub(right.multiplyScalar(cameraSpeed));
|
||||||
|
}
|
||||||
|
if (event.code === 'KeyD' || event.code === 'KeyВ') {
|
||||||
|
const right = new THREE.Vector3();
|
||||||
|
camera.getWorldDirection(right);
|
||||||
|
right.cross(camera.up).normalize();
|
||||||
|
camera.position.add(right.multiplyScalar(cameraSpeed));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Управление выбранным объектом
|
||||||
|
const obj = selectedRef.current;
|
||||||
|
if (obj) {
|
||||||
|
if (event.code === 'ArrowUp') {
|
||||||
|
obj.position.z += objectMoveSpeed;
|
||||||
|
updateObjectCoordinates(obj);
|
||||||
|
}
|
||||||
|
if (event.code === 'ArrowDown') {
|
||||||
|
obj.position.z -= objectMoveSpeed;
|
||||||
|
updateObjectCoordinates(obj);
|
||||||
|
}
|
||||||
|
if (event.code === 'ArrowLeft') {
|
||||||
|
obj.position.x -= objectMoveSpeed;
|
||||||
|
updateObjectCoordinates(obj);
|
||||||
|
}
|
||||||
|
if (event.code === 'ArrowRight') {
|
||||||
|
obj.position.x += objectMoveSpeed;
|
||||||
|
updateObjectCoordinates(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = (event) => {
|
||||||
|
keysPressed.current[event.code] = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
document.addEventListener('keyup', handleKeyUp);
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
@@ -105,7 +188,10 @@ export default function MapEditor() {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
renderer.domElement.removeEventListener('pointerdown', onPointerDown);
|
renderer.domElement.removeEventListener('pointerdown', onPointerDown);
|
||||||
|
renderer.domElement.removeEventListener('click', onRulerClick);
|
||||||
window.removeEventListener('resize', onResize);
|
window.removeEventListener('resize', onResize);
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
document.removeEventListener('keyup', handleKeyUp);
|
||||||
mountRef.current.removeChild(renderer.domElement);
|
mountRef.current.removeChild(renderer.domElement);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -114,6 +200,60 @@ export default function MapEditor() {
|
|||||||
transformRef.current?.setMode(mode);
|
transformRef.current?.setMode(mode);
|
||||||
}, [mode]);
|
}, [mode]);
|
||||||
|
|
||||||
|
// Отдельный useEffect для обработки клавиш
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
// Копирование и вставка
|
||||||
|
if (event.ctrlKey && event.code === 'KeyC') {
|
||||||
|
event.preventDefault();
|
||||||
|
const obj = selectedRef.current;
|
||||||
|
if (obj) {
|
||||||
|
setCopiedObject({
|
||||||
|
model_url: obj.userData.model_url,
|
||||||
|
name: obj.userData.name,
|
||||||
|
position: obj.position.clone(),
|
||||||
|
rotation: obj.rotation.clone(),
|
||||||
|
scale: obj.scale.clone()
|
||||||
|
});
|
||||||
|
console.log('Объект скопирован:', obj.userData.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.ctrlKey && event.code === 'KeyV') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (copiedObject) {
|
||||||
|
console.log('Вставка объекта:', copiedObject.name);
|
||||||
|
loader.load(copiedObject.model_url, gltf => {
|
||||||
|
const m = gltf.scene;
|
||||||
|
m.userData = {
|
||||||
|
model_url: copiedObject.model_url,
|
||||||
|
name: copiedObject.name + '_copy'
|
||||||
|
};
|
||||||
|
m.position.copy(copiedObject.position);
|
||||||
|
m.rotation.copy(copiedObject.rotation);
|
||||||
|
m.scale.copy(copiedObject.scale);
|
||||||
|
m.traverse(child => {
|
||||||
|
if (child.isMesh && materialRef.current) {
|
||||||
|
child.material = materialRef.current.clone();
|
||||||
|
child.material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sceneRef.current.add(m);
|
||||||
|
objectsRef.current.push(m);
|
||||||
|
transformRef.current.attach(m);
|
||||||
|
selectedRef.current = m;
|
||||||
|
updateObjectCoordinates(m);
|
||||||
|
console.log('✅ Объект скопирован (сохранение при нажатии "Сохранить")');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [copiedObject, loader]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
fetch('/api/models', { headers: { Authorization: `Bearer ${token}` } })
|
fetch('/api/models', { headers: { Authorization: `Bearer ${token}` } })
|
||||||
@@ -133,22 +273,43 @@ export default function MapEditor() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!cityId) return;
|
if (!cityId) return;
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
// очистка текущих объектов
|
// очистка текущих объектов
|
||||||
objectsRef.current.forEach(o => sceneRef.current.remove(o));
|
objectsRef.current.forEach(o => sceneRef.current.remove(o));
|
||||||
objectsRef.current = [];
|
objectsRef.current = [];
|
||||||
removedIdsRef.current = [];
|
removedIdsRef.current = [];
|
||||||
|
|
||||||
|
console.log('📡 Запрашиваем объекты с сервера...');
|
||||||
fetch(`/api/cities/${cityId}/objects`, {
|
fetch(`/api/cities/${cityId}/objects`, {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => {
|
||||||
|
console.log('📡 Ответ сервера на загрузку объектов:', r.status, r.statusText);
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
console.log('📊 Загружено объектов из БД:', data.length);
|
||||||
|
console.log('📋 Данные объектов:', data);
|
||||||
|
|
||||||
data.forEach(obj => {
|
data.forEach(obj => {
|
||||||
|
console.log('🔄 Загружаем объект:', obj.name, 'ID:', obj.id);
|
||||||
loader.load(obj.model_url, gltf => {
|
loader.load(obj.model_url, gltf => {
|
||||||
const m = gltf.scene;
|
const m = gltf.scene;
|
||||||
m.position.set(obj.pos_x, obj.pos_y, obj.pos_z);
|
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.rotation.set(obj.rot_x, obj.rot_y, obj.rot_z);
|
||||||
m.userData = { id: obj.id, model_url: obj.model_url, name: obj.name };
|
m.scale.set(obj.scale_x, obj.scale_y, obj.scale_z);
|
||||||
|
m.userData = {
|
||||||
|
id: obj.id,
|
||||||
|
model_url: obj.model_url,
|
||||||
|
name: obj.name,
|
||||||
|
organization_id: obj.organization_id,
|
||||||
|
rent: obj.rent,
|
||||||
|
tax: obj.tax,
|
||||||
|
collidable: obj.collidable,
|
||||||
|
interior_id: obj.interior_id,
|
||||||
|
textures: obj.textures
|
||||||
|
};
|
||||||
m.traverse(child => {
|
m.traverse(child => {
|
||||||
if (child.isMesh && materialRef.current) {
|
if (child.isMesh && materialRef.current) {
|
||||||
child.material = materialRef.current.clone();
|
child.material = materialRef.current.clone();
|
||||||
@@ -160,7 +321,10 @@ export default function MapEditor() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Ошибка загрузки объектов', err));
|
.catch(err => {
|
||||||
|
console.error('❌ Ошибка загрузки объектов:', err);
|
||||||
|
alert(`Ошибка загрузки объектов: ${err.message}`);
|
||||||
|
});
|
||||||
}, [cityId, loader]);
|
}, [cityId, loader]);
|
||||||
|
|
||||||
const addModel = name => {
|
const addModel = name => {
|
||||||
@@ -175,31 +339,162 @@ export default function MapEditor() {
|
|||||||
child.material.needsUpdate = true;
|
child.material.needsUpdate = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Размещаем объект перед камерой
|
||||||
|
const camera = cameraRef.current;
|
||||||
|
const direction = new THREE.Vector3();
|
||||||
|
camera.getWorldDirection(direction);
|
||||||
|
const distance = 5; // расстояние от камеры
|
||||||
|
m.position.copy(camera.position).add(direction.multiplyScalar(distance));
|
||||||
|
|
||||||
sceneRef.current.add(m);
|
sceneRef.current.add(m);
|
||||||
objectsRef.current.push(m);
|
objectsRef.current.push(m);
|
||||||
transformRef.current.attach(m);
|
transformRef.current.attach(m);
|
||||||
|
selectedRef.current = m;
|
||||||
|
updateObjectCoordinates(m);
|
||||||
|
|
||||||
|
console.log('✅ Объект добавлен в сцену (сохранение при нажатии "Сохранить")');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSelected = () => {
|
const copySelectedObject = () => {
|
||||||
const obj = selectedRef.current;
|
const obj = selectedRef.current;
|
||||||
if (!obj) return;
|
if (!obj) return;
|
||||||
transformRef.current.detach();
|
setCopiedObject({
|
||||||
if (obj.parent) {
|
model_url: obj.userData.model_url,
|
||||||
obj.parent.remove(obj);
|
name: obj.userData.name,
|
||||||
} else {
|
position: obj.position.clone(),
|
||||||
sceneRef.current.remove(obj);
|
rotation: obj.rotation.clone(),
|
||||||
}
|
scale: obj.scale.clone()
|
||||||
objectsRef.current = objectsRef.current.filter(o => o !== obj);
|
});
|
||||||
if (obj.userData.id) {
|
|
||||||
removedIdsRef.current.push(obj.userData.id);
|
|
||||||
}
|
|
||||||
selectedRef.current = null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveMap = () => {
|
const pasteObject = () => {
|
||||||
const objects = objectsRef.current.map(obj => ({
|
if (!copiedObject) return;
|
||||||
|
loader.load(copiedObject.model_url, gltf => {
|
||||||
|
const m = gltf.scene;
|
||||||
|
m.userData = {
|
||||||
|
model_url: copiedObject.model_url,
|
||||||
|
name: copiedObject.name + '_copy'
|
||||||
|
};
|
||||||
|
m.position.copy(copiedObject.position);
|
||||||
|
m.rotation.copy(copiedObject.rotation);
|
||||||
|
m.scale.copy(copiedObject.scale);
|
||||||
|
m.traverse(child => {
|
||||||
|
if (child.isMesh && materialRef.current) {
|
||||||
|
child.material = materialRef.current.clone();
|
||||||
|
child.material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sceneRef.current.add(m);
|
||||||
|
objectsRef.current.push(m);
|
||||||
|
transformRef.current.attach(m);
|
||||||
|
selectedRef.current = m;
|
||||||
|
updateObjectCoordinates(m);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateObjectCoordinates = (obj) => {
|
||||||
|
if (!obj) return;
|
||||||
|
|
||||||
|
// Безопасное получение координат
|
||||||
|
const posX = typeof obj.position.x === 'number' ? obj.position.x : 0;
|
||||||
|
const posY = typeof obj.position.y === 'number' ? obj.position.y : 0;
|
||||||
|
const posZ = typeof obj.position.z === 'number' ? obj.position.z : 0;
|
||||||
|
|
||||||
|
setObjectCoordinates({
|
||||||
|
x: parseFloat(posX.toFixed(3)),
|
||||||
|
y: parseFloat(posY.toFixed(3)),
|
||||||
|
z: parseFloat(posZ.toFixed(3))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Безопасное получение поворота
|
||||||
|
const rotX = typeof obj.rotation.x === 'number' ? obj.rotation.x : 0;
|
||||||
|
const rotY = typeof obj.rotation.y === 'number' ? obj.rotation.y : 0;
|
||||||
|
const rotZ = typeof obj.rotation.z === 'number' ? obj.rotation.z : 0;
|
||||||
|
|
||||||
|
setObjectRotation({
|
||||||
|
x: parseFloat((rotX * 180 / Math.PI).toFixed(3)),
|
||||||
|
y: parseFloat((rotY * 180 / Math.PI).toFixed(3)),
|
||||||
|
z: parseFloat((rotZ * 180 / Math.PI).toFixed(3))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Безопасное получение масштаба
|
||||||
|
const scaleX = typeof obj.scale.x === 'number' ? obj.scale.x : 1;
|
||||||
|
const scaleY = typeof obj.scale.y === 'number' ? obj.scale.y : 1;
|
||||||
|
const scaleZ = typeof obj.scale.z === 'number' ? obj.scale.z : 1;
|
||||||
|
|
||||||
|
setObjectScale({
|
||||||
|
x: parseFloat(scaleX.toFixed(3)),
|
||||||
|
y: parseFloat(scaleY.toFixed(3)),
|
||||||
|
z: parseFloat(scaleZ.toFixed(3))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем дополнительные свойства
|
||||||
|
setObjectId(obj.userData.id || null);
|
||||||
|
setObjectName(obj.userData.name || '');
|
||||||
|
setObjectOrganizationId(obj.userData.organization_id || 2);
|
||||||
|
setObjectCollidable(obj.userData.collidable || false);
|
||||||
|
setObjectInteriorId(obj.userData.interior_id || 101);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyCoordinates = () => {
|
||||||
|
const obj = selectedRef.current;
|
||||||
|
if (!obj) return;
|
||||||
|
obj.position.set(objectCoordinates.x, objectCoordinates.y, objectCoordinates.z);
|
||||||
|
obj.rotation.set(
|
||||||
|
objectRotation.x * Math.PI / 180,
|
||||||
|
objectRotation.y * Math.PI / 180,
|
||||||
|
objectRotation.z * Math.PI / 180
|
||||||
|
);
|
||||||
|
obj.scale.set(objectScale.x, objectScale.y, objectScale.z);
|
||||||
|
|
||||||
|
// Обновляем дополнительные свойства в userData
|
||||||
|
obj.userData.name = objectName;
|
||||||
|
obj.userData.organization_id = objectOrganizationId;
|
||||||
|
obj.userData.collidable = objectCollidable;
|
||||||
|
obj.userData.interior_id = objectInteriorId;
|
||||||
|
|
||||||
|
console.log('🔧 Обновлены свойства объекта:', {
|
||||||
|
name: objectName,
|
||||||
|
organization_id: objectOrganizationId,
|
||||||
|
collidable: objectCollidable,
|
||||||
|
interior_id: objectInteriorId
|
||||||
|
});
|
||||||
|
|
||||||
|
transformRef.current.updateMatrixWorld();
|
||||||
|
|
||||||
|
console.log('✅ Координаты объекта обновлены (сохранение при нажатии "Сохранить")');
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveObjectToDatabase = async (obj) => {
|
||||||
|
console.log('🔄 Начинаем сохранение объекта в БД...');
|
||||||
|
console.log('📊 Данные объекта:', {
|
||||||
id: obj.userData.id,
|
id: obj.userData.id,
|
||||||
|
name: obj.userData.name,
|
||||||
|
model_url: obj.userData.model_url,
|
||||||
|
position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
|
||||||
|
cityId: cityId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cityId) {
|
||||||
|
console.error('❌ cityId не установлен, пропускаем сохранение');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
console.error('❌ Токен не найден');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если у объекта есть ID, то обновляем, иначе создаем новый
|
||||||
|
const isUpdate = obj.userData.id && obj.userData.id !== null;
|
||||||
|
console.log(`📝 ${isUpdate ? 'Обновляем' : 'Создаем'} объект:`, obj.userData.name);
|
||||||
|
|
||||||
|
const objectData = {
|
||||||
|
id: obj.userData.id, // null для новых объектов
|
||||||
|
city_id: cityId,
|
||||||
name: obj.userData.name || '',
|
name: obj.userData.name || '',
|
||||||
model_url: obj.userData.model_url,
|
model_url: obj.userData.model_url,
|
||||||
pos_x: obj.position.x,
|
pos_x: obj.position.x,
|
||||||
@@ -210,27 +505,154 @@ export default function MapEditor() {
|
|||||||
rot_z: obj.rotation.z,
|
rot_z: obj.rotation.z,
|
||||||
scale_x: obj.scale.x,
|
scale_x: obj.scale.x,
|
||||||
scale_y: obj.scale.y,
|
scale_y: obj.scale.y,
|
||||||
scale_z: obj.scale.z
|
scale_z: obj.scale.z,
|
||||||
}));
|
organization_id: obj.userData.organization_id || 2,
|
||||||
|
rent: obj.userData.rent || 0,
|
||||||
|
tax: obj.userData.tax || 0,
|
||||||
|
collidable: obj.userData.collidable || false,
|
||||||
|
interior_id: obj.userData.interior_id || 101,
|
||||||
|
textures: obj.userData.textures || '/packs/citypack.json'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📤 Отправляем данные на сервер:', objectData);
|
||||||
|
|
||||||
|
console.log('🔍 Проверяем коллизию:', {
|
||||||
|
'obj.userData.collidable': obj.userData.collidable,
|
||||||
|
'objectData.collidable': objectData.collidable,
|
||||||
|
'objectCollidable state': objectCollidable
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/save-object', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(objectData)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📡 Ответ сервера:', response.status, response.statusText);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('✅ Объект успешно сохранен в БД:', result);
|
||||||
|
if (result.id && !obj.userData.id) {
|
||||||
|
obj.userData.id = result.id;
|
||||||
|
setObjectId(result.id);
|
||||||
|
console.log('🆔 Назначен ID объекту:', result.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('❌ Ошибка сохранения объекта:', response.status, errorText);
|
||||||
|
alert(`Ошибка сохранения: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка сети при сохранении:', error);
|
||||||
|
alert(`Ошибка сети: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteObjectFromDatabase = async (objId) => {
|
||||||
|
if (!objId) return;
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
fetch('/api/save-map', {
|
try {
|
||||||
method: 'POST',
|
await fetch(`/api/delete-object/${objId}`, {
|
||||||
headers: {
|
method: 'DELETE',
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
Authorization: `Bearer ${token}`
|
Authorization: `Bearer ${token}`
|
||||||
},
|
}
|
||||||
body: JSON.stringify({ cityId, objects, removedIds: removedIdsRef.current })
|
});
|
||||||
})
|
} catch (error) {
|
||||||
.then(r => {
|
console.error('Ошибка удаления объекта:', error);
|
||||||
if (!r.ok) throw new Error('fail');
|
}
|
||||||
alert('Карта сохранена');
|
};
|
||||||
})
|
|
||||||
.catch(() => alert('Ошибка сохранения'));
|
const deleteSelected = async () => {
|
||||||
|
const obj = selectedRef.current;
|
||||||
|
if (!obj) return;
|
||||||
|
transformRef.current.detach();
|
||||||
|
|
||||||
|
// Удаляем из базы данных
|
||||||
|
if (obj.userData.id) {
|
||||||
|
await deleteObjectFromDatabase(obj.userData.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.parent) {
|
||||||
|
obj.parent.remove(obj);
|
||||||
|
} else {
|
||||||
|
sceneRef.current.remove(obj);
|
||||||
|
}
|
||||||
|
objectsRef.current = objectsRef.current.filter(o => o !== obj);
|
||||||
|
selectedRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveMap = async () => {
|
||||||
|
console.log('🔄 Сохраняем все объекты в БД...');
|
||||||
|
|
||||||
|
if (!cityId) {
|
||||||
|
alert('Город не выбран');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
alert('Токен не найден');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Сохраняем все объекты в БД
|
||||||
|
const savePromises = objectsRef.current.map(obj => saveObjectToDatabase(obj));
|
||||||
|
await Promise.all(savePromises);
|
||||||
|
|
||||||
|
console.log('✅ Все объекты сохранены в БД');
|
||||||
|
alert('Карта сохранена в базу данных');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка сохранения карты:', error);
|
||||||
|
alert('Ошибка сохранения карты');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleRulerMode = () => {
|
||||||
|
setRulerMode(!rulerMode);
|
||||||
|
setRulerPoints([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRulerClick = (event) => {
|
||||||
|
if (!rulerMode) return;
|
||||||
|
|
||||||
|
const rect = rendererRef.current.domElement.getBoundingClientRect();
|
||||||
|
const mouse = new THREE.Vector2();
|
||||||
|
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
|
|
||||||
|
const raycaster = new THREE.Raycaster();
|
||||||
|
raycaster.setFromCamera(mouse, cameraRef.current);
|
||||||
|
|
||||||
|
// Создаем плоскость на уровне Y=0 для измерения
|
||||||
|
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
||||||
|
const intersection = new THREE.Vector3();
|
||||||
|
raycaster.ray.intersectPlane(plane, intersection);
|
||||||
|
|
||||||
|
setRulerPoints(prev => [...prev, intersection]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateDistance = () => {
|
||||||
|
if (rulerPoints.length < 2) return 0;
|
||||||
|
const point1 = rulerPoints[0];
|
||||||
|
const point2 = rulerPoints[1];
|
||||||
|
return Math.sqrt(
|
||||||
|
Math.pow(point2.x - point1.x, 2) +
|
||||||
|
Math.pow(point2.z - point1.z, 2)
|
||||||
|
).toFixed(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%', height: '100vh', position: 'relative' }} ref={mountRef}>
|
<div style={{ width: '100%', height: '100vh', position: 'relative' }} ref={mountRef}>
|
||||||
<div style={{ position: 'absolute', top: 10, left: 10, background: 'rgba(255,255,255,0.8)', padding: 8 }}>
|
{/* Левая панель управления */}
|
||||||
|
<div style={{ position: 'absolute', top: 10, left: 10, background: 'rgba(255,255,255,0.9)', padding: 8, borderRadius: 4 }}>
|
||||||
<div style={{ marginBottom: 4 }}>
|
<div style={{ marginBottom: 4 }}>
|
||||||
<select value={cityId || ''} onChange={e => setCityId(Number(e.target.value))}>
|
<select value={cityId || ''} onChange={e => setCityId(Number(e.target.value))}>
|
||||||
{cities.map(c => (
|
{cities.map(c => (
|
||||||
@@ -248,11 +670,215 @@ export default function MapEditor() {
|
|||||||
const select = document.getElementById('modelSelect');
|
const select = document.getElementById('modelSelect');
|
||||||
addModel(select.value);
|
addModel(select.value);
|
||||||
}}>Добавить</button>
|
}}>Добавить</button>
|
||||||
<button onClick={() => setMode(mode === 'translate' ? 'rotate' : 'translate')}>
|
<button onClick={() => {
|
||||||
{mode === 'translate' ? 'Перемещение' : 'Вращение'}
|
if (mode === 'translate') setMode('rotate');
|
||||||
|
else if (mode === 'rotate') setMode('scale');
|
||||||
|
else setMode('translate');
|
||||||
|
}}>
|
||||||
|
{mode === 'translate' ? 'Перемещение' : mode === 'rotate' ? 'Вращение' : 'Масштаб'}
|
||||||
</button>
|
</button>
|
||||||
|
<button onClick={copySelectedObject}>Копировать</button>
|
||||||
|
<button onClick={pasteObject} disabled={!copiedObject}>Вставить</button>
|
||||||
<button onClick={deleteSelected}>Удалить</button>
|
<button onClick={deleteSelected}>Удалить</button>
|
||||||
|
|
||||||
<button onClick={saveMap}>Сохранить</button>
|
<button onClick={saveMap}>Сохранить</button>
|
||||||
|
<button
|
||||||
|
onClick={() => window.open('/model-gallery', '_blank')}
|
||||||
|
style={{ background: '#28a745', color: 'white' }}
|
||||||
|
>
|
||||||
|
Галерея моделей
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Правая панель координат */}
|
||||||
|
<div style={{ position: 'absolute', top: 10, right: 10, background: 'rgba(255,255,255,0.9)', padding: 8, borderRadius: 4, minWidth: 200 }}>
|
||||||
|
<h4 style={{ margin: '0 0 8px 0', fontSize: 14 }}>Координаты объекта</h4>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<label style={{ fontSize: 12, display: 'block' }}>Позиция:</label>
|
||||||
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={objectCoordinates.x}
|
||||||
|
onChange={e => setObjectCoordinates({...objectCoordinates, x: parseFloat(e.target.value) || 0})}
|
||||||
|
style={{ width: 50, fontSize: 11 }}
|
||||||
|
placeholder="X"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={objectCoordinates.y}
|
||||||
|
onChange={e => setObjectCoordinates({...objectCoordinates, y: parseFloat(e.target.value) || 0})}
|
||||||
|
style={{ width: 50, fontSize: 11 }}
|
||||||
|
placeholder="Y"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={objectCoordinates.z}
|
||||||
|
onChange={e => setObjectCoordinates({...objectCoordinates, z: parseFloat(e.target.value) || 0})}
|
||||||
|
style={{ width: 50, fontSize: 11 }}
|
||||||
|
placeholder="Z"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<label style={{ fontSize: 12, display: 'block' }}>Поворот (градусы):</label>
|
||||||
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
value={objectRotation.x}
|
||||||
|
onChange={e => setObjectRotation({...objectRotation, x: parseFloat(e.target.value) || 0})}
|
||||||
|
style={{ width: 50, fontSize: 11 }}
|
||||||
|
placeholder="X"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
value={objectRotation.y}
|
||||||
|
onChange={e => setObjectRotation({...objectRotation, y: parseFloat(e.target.value) || 0})}
|
||||||
|
style={{ width: 50, fontSize: 11 }}
|
||||||
|
placeholder="Y"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
value={objectRotation.z}
|
||||||
|
onChange={e => setObjectRotation({...objectRotation, z: parseFloat(e.target.value) || 0})}
|
||||||
|
style={{ width: 50, fontSize: 11 }}
|
||||||
|
placeholder="Z"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<label style={{ fontSize: 12, display: 'block' }}>Масштаб:</label>
|
||||||
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={objectScale.x}
|
||||||
|
onChange={e => setObjectScale({...objectScale, x: parseFloat(e.target.value) || 1})}
|
||||||
|
style={{ width: 50, fontSize: 11 }}
|
||||||
|
placeholder="X"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={objectScale.y}
|
||||||
|
onChange={e => setObjectScale({...objectScale, y: parseFloat(e.target.value) || 1})}
|
||||||
|
style={{ width: 50, fontSize: 11 }}
|
||||||
|
placeholder="Y"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={objectScale.z}
|
||||||
|
onChange={e => setObjectScale({...objectScale, z: parseFloat(e.target.value) || 1})}
|
||||||
|
style={{ width: 50, fontSize: 11 }}
|
||||||
|
placeholder="Z"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={applyCoordinates} style={{ fontSize: 11, padding: '4px 8px' }}>
|
||||||
|
Применить
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Расширенные свойства */}
|
||||||
|
<div style={{ marginTop: 12, borderTop: '1px solid #ccc', paddingTop: 8 }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdvancedProperties(!showAdvancedProperties)}
|
||||||
|
style={{ fontSize: 11, padding: '4px 8px', marginBottom: 8 }}
|
||||||
|
>
|
||||||
|
{showAdvancedProperties ? 'Скрыть' : 'Показать'} дополнительные свойства
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showAdvancedProperties && (
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<label style={{ fontSize: 12, display: 'block' }}>ID объекта:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={objectId || ''}
|
||||||
|
disabled
|
||||||
|
style={{ width: 100, fontSize: 11 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<label style={{ fontSize: 12, display: 'block' }}>Название:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={objectName}
|
||||||
|
onChange={e => setObjectName(e.target.value)}
|
||||||
|
style={{ width: 100, fontSize: 11 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<label style={{ fontSize: 12, display: 'block' }}>ID организации:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={objectOrganizationId}
|
||||||
|
onChange={e => setObjectOrganizationId(parseInt(e.target.value) || 2)}
|
||||||
|
style={{ width: 100, fontSize: 11 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<label style={{ fontSize: 12, display: 'block' }}>ID интерьера:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={objectInteriorId}
|
||||||
|
onChange={e => setObjectInteriorId(parseInt(e.target.value) || 101)}
|
||||||
|
style={{ width: 100, fontSize: 11 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<label style={{ fontSize: 12, display: 'flex', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={objectCollidable}
|
||||||
|
onChange={e => setObjectCollidable(e.target.checked)}
|
||||||
|
style={{ marginRight: 4 }}
|
||||||
|
/>
|
||||||
|
Коллизия
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Панель линейки */}
|
||||||
|
{rulerMode && (
|
||||||
|
<div style={{ position: 'absolute', top: 10, left: '50%', transform: 'translateX(-50%)', background: 'rgba(255,255,255,0.9)', padding: 8, borderRadius: 4, fontSize: 12 }}>
|
||||||
|
<div>Режим линейки активен</div>
|
||||||
|
<div>Кликните на две точки для измерения расстояния</div>
|
||||||
|
{rulerPoints.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div>Точек: {rulerPoints.length}/2</div>
|
||||||
|
{rulerPoints.length === 2 && (
|
||||||
|
<div>Расстояние: {calculateDistance()} единиц</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Панель с подсказками */}
|
||||||
|
<div style={{ position: 'absolute', bottom: 10, left: 10, background: 'rgba(0,0,0,0.7)', color: 'white', padding: 8, borderRadius: 4, fontSize: 12 }}>
|
||||||
|
<div>WASD/ЦФЫВ - перемещение камеры</div>
|
||||||
|
<div>Стрелки - перемещение объекта</div>
|
||||||
|
<div>Ctrl+C - копировать, Ctrl+V - вставить</div>
|
||||||
|
{rulerMode && <div>Клик - добавить точку линейки</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
353
src/pages/ModelGallery.jsx
Normal file
353
src/pages/ModelGallery.jsx
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import ModelThumbnail from '../components/ModelThumbnail';
|
||||||
|
import ModelPreview from '../components/ModelPreview';
|
||||||
|
|
||||||
|
const ModelGallery = () => {
|
||||||
|
const [models, setModels] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [selectedModel, setSelectedModel] = useState(null);
|
||||||
|
const [filteredModels, setFilteredModels] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadModels();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filtered = models.filter(model =>
|
||||||
|
model.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
setFilteredModels(filtered);
|
||||||
|
}, [models, searchTerm]);
|
||||||
|
|
||||||
|
const loadModels = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const response = await fetch('/api/models', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const modelList = await response.json();
|
||||||
|
setModels(modelList);
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
console.error('Ошибка загрузки моделей');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки моделей:', error);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModelClick = (modelName) => {
|
||||||
|
setSelectedModel(selectedModel === modelName ? null : modelName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModelDisplayName = (modelName) => {
|
||||||
|
return modelName
|
||||||
|
.replace(/\.glb$/, '')
|
||||||
|
.replace(/[-_]/g, ' ')
|
||||||
|
.replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModelUrl = (modelName) => {
|
||||||
|
return `/models/copied/${modelName}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100vh',
|
||||||
|
fontSize: '18px'
|
||||||
|
}}>
|
||||||
|
Загрузка моделей...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<div style={{
|
||||||
|
padding: '20px',
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '0 auto',
|
||||||
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
minHeight: '100vh',
|
||||||
|
overflowY: 'auto'
|
||||||
|
}}>
|
||||||
|
<h1 style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: '30px',
|
||||||
|
color: '#333'
|
||||||
|
}}>
|
||||||
|
Галерея моделей
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Поиск */}
|
||||||
|
<div style={{
|
||||||
|
marginBottom: '30px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Поиск моделей..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
style={{
|
||||||
|
padding: '10px 15px',
|
||||||
|
fontSize: '16px',
|
||||||
|
border: '2px solid #ddd',
|
||||||
|
borderRadius: '25px',
|
||||||
|
width: '300px',
|
||||||
|
outline: 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Статистика */}
|
||||||
|
<div style={{
|
||||||
|
marginBottom: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
Показано {filteredModels.length} из {models.length} моделей
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Сетка моделей */}
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||||
|
gap: '20px',
|
||||||
|
marginBottom: '40px'
|
||||||
|
}}>
|
||||||
|
{filteredModels.map((modelName, index) => (
|
||||||
|
<div
|
||||||
|
key={modelName}
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: 'transform 0.2s, box-shadow 0.2s',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: selectedModel === modelName ? '3px solid #007bff' : '1px solid #ddd'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateY(-5px)';
|
||||||
|
e.currentTarget.style.boxShadow = '0 8px 15px rgba(0, 0, 0, 0.2)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
|
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
|
||||||
|
}}
|
||||||
|
onClick={() => handleModelClick(modelName)}
|
||||||
|
>
|
||||||
|
{/* PNG превью */}
|
||||||
|
<ModelThumbnail
|
||||||
|
modelUrl={getModelUrl(modelName)}
|
||||||
|
modelName={modelName}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Информация о модели */}
|
||||||
|
<div style={{ padding: '15px' }}>
|
||||||
|
<h3 style={{
|
||||||
|
margin: '0 0 10px 0',
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: '600'
|
||||||
|
}}>
|
||||||
|
{getModelDisplayName(modelName)}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#666',
|
||||||
|
marginBottom: '10px'
|
||||||
|
}}>
|
||||||
|
Файл: {modelName}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Кнопки действий */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
marginTop: '15px'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: '#007bff',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '12px'
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Здесь можно добавить логику для использования модели в редакторе
|
||||||
|
alert(`Модель "${getModelDisplayName(modelName)}" выбрана для редактора`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Использовать
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: '#6c757d',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '12px'
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Скачать модель
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = getModelUrl(modelName);
|
||||||
|
link.download = modelName;
|
||||||
|
link.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Скачать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Модальное окно для детального просмотра */}
|
||||||
|
{selectedModel && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
zIndex: 1000
|
||||||
|
}}
|
||||||
|
onClick={() => setSelectedModel(null)}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '20px',
|
||||||
|
maxWidth: '80vw',
|
||||||
|
maxHeight: '80vh',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '10px',
|
||||||
|
right: '10px',
|
||||||
|
background: '#dc3545',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: '30px',
|
||||||
|
height: '30px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '16px'
|
||||||
|
}}
|
||||||
|
onClick={() => setSelectedModel(null)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2 style={{ marginTop: 0, marginBottom: '20px' }}>
|
||||||
|
{getModelDisplayName(selectedModel)}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style={{ height: '400px', width: '600px' }}>
|
||||||
|
<ModelPreview
|
||||||
|
modelUrl={getModelUrl(selectedModel)}
|
||||||
|
modelName={selectedModel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '20px' }}>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#28a745',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
marginRight: '10px'
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
alert(`Модель "${getModelDisplayName(selectedModel)}" выбрана для редактора`);
|
||||||
|
setSelectedModel(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Использовать в редакторе
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#6c757d',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = getModelUrl(selectedModel);
|
||||||
|
link.download = selectedModel;
|
||||||
|
link.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Скачать модель
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Подвал */}
|
||||||
|
<div style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: '40px',
|
||||||
|
padding: '20px',
|
||||||
|
background: '#f8f9fa',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
<p>Всего моделей: {models.length}</p>
|
||||||
|
<p>Используйте колесо мыши для масштабирования, зажмите и тяните для поворота</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelGallery;
|
||||||
456
src/pages/QuestSystem .jsx
Normal file
456
src/pages/QuestSystem .jsx
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
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');
|
||||||
|
console.log('Токен для квеста:', token ? 'присутствует' : 'отсутствует');
|
||||||
|
console.log('ID квеста:', questId);
|
||||||
|
|
||||||
|
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
545
src/pages/README.md
Normal 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
70
src/test-collision.js
Normal 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 };
|
||||||
131
test-advanced-collision-editor.js
Normal file
131
test-advanced-collision-editor.js
Normal 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
73
test-camera-controls.js
Normal 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
53
test-collider-creation.js
Normal 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('✅ Готово к тестированию!');
|
||||||
|
|
||||||
141
test-collider-deletion-debug.js
Normal file
141
test-collider-deletion-debug.js
Normal 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('Откройте консоль браузера для просмотра логов');
|
||||||
174
test-collider-deletion-fix.js
Normal file
174
test-collider-deletion-fix.js
Normal 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('Попробуйте удалить коллайдер и проверить, что он не появляется после перезагрузки');
|
||||||
160
test-collider-position-saving.js
Normal file
160
test-collider-position-saving.js
Normal 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
97
test-colliders.js
Normal 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) - синий цвет всех объектов (для сравнения)');
|
||||||
145
test-collision-editor-error-fixes.js
Normal file
145
test-collision-editor-error-fixes.js
Normal 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('Проверьте, что все коллайдеры загружаются и работают корректно');
|
||||||
129
test-collision-editor-fixes.js
Normal file
129
test-collision-editor-fixes.js
Normal 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
405
test-collision.html
Normal 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>
|
||||||
173
test-complete-collision-editor-improvements.js
Normal file
173
test-complete-collision-editor-improvements.js
Normal 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');
|
||||||
135
test-duplicate-transform-fix.js
Normal file
135
test-duplicate-transform-fix.js
Normal 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('Проверьте дублирование коллайдеров с различными параметрами');
|
||||||
167
test-game-collider-visibility-fix.js
Normal file
167
test-game-collider-visibility-fix.js
Normal 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('Протестируйте прозрачность коллизионных объектов и функции отладки');
|
||||||
153
test-game-colliders-loading.js
Normal file
153
test-game-colliders-loading.js
Normal 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
297
test-interior-api.html
Normal 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>
|
||||||
176
test-opacity-collision-fixes.js
Normal file
176
test-opacity-collision-fixes.js
Normal 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
164
test-wireframe-fix.js
Normal 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('Попробуйте кликнуть по рамке коллайдера и переместить его');
|
||||||
Reference in New Issue
Block a user