init commit
This commit is contained in:
commit
f716b05e18
7 changed files with 2766 additions and 0 deletions
181
README.md
Normal file
181
README.md
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
# 🌍 Geo-Step Game - Настольная игра-путешествие по миру
|
||||
|
||||
Образовательная настольная игра, где вы изучаете карту мира, путешествуя по континентам и выполняя задания по доставке грузов.
|
||||
|
||||
## 🎮 Описание игры
|
||||
|
||||
**Geo-Step Game** — это многопользовательская настольная игра для 1-4 игроков. Цель игры — изучить карту мира, передвигаясь между городами, портами и логистическими хабами, выполняя задания по доставке грузов и набирая очки.
|
||||
|
||||
## ✨ Особенности
|
||||
|
||||
- 🗺️ **Реалистичная карта мира** с физической картой в качестве подложки
|
||||
- 👥 **Многопользовательский режим** для 1-4 игроков
|
||||
- 🎲 **Система костей** — бросайте две кости, чтобы определить количество шагов
|
||||
- 🚢 **Кораблики игроков** — у каждого свой уникальный корабль (🚢 ⛴️ 🛳️ ⛵)
|
||||
- 🗺️ **200+ путевых точек** на основных торговых маршрутах
|
||||
- 🌊 **Альтернативные маршруты** — Суэц или вокруг Африки, Панама или Мыс Горн
|
||||
- 📦 **Множественные задания** — перевозите несколько грузов одновременно
|
||||
- ⚖️ **Вес и объем** — управляйте вместимостью корабля (500 кг / 600 м³)
|
||||
- ✅ **Подсветка заданий** — видите какой груз везете и куда
|
||||
- 🏆 **Система очков** — набирайте очки за выполнение заданий
|
||||
- 🎯 **Обучающий процесс** — запоминайте географию в процессе игры
|
||||
|
||||
## 🎯 Правила игры
|
||||
|
||||
### Начало игры
|
||||
1. Выберите количество игроков (1-4)
|
||||
2. Введите имена игроков
|
||||
3. Все игроки начинают в **Нью-Йорке**
|
||||
|
||||
### Ход игрока
|
||||
1. **Бросьте кости** 🎲 — выпадет число от 2 до 12
|
||||
2. На карте подсветятся **доступные локации** на расстоянии выпавших шагов
|
||||
3. **Кликните на локацию**, куда хотите пойти
|
||||
4. Нажмите **"Закончить ход"**
|
||||
|
||||
### Задания
|
||||
- Можете брать **несколько заданий** одновременно
|
||||
- Задание состоит из:
|
||||
- 📍 Пункта отправления
|
||||
- 📍 Пункта назначения
|
||||
- 📦 Типа груза
|
||||
- ⚖️ Веса и объема
|
||||
- 💰 Награды в очках
|
||||
- **Вместимость корабля ограничена:**
|
||||
- Максимальный вес: 600 кг
|
||||
- Максимальный объем: 700 м³
|
||||
- Доставляйте грузы попутно, планируя маршрут
|
||||
|
||||
### Цель игры
|
||||
**Первым набрать 5000 очков**, выполняя задания по доставке грузов! 🏆
|
||||
|
||||
Прогресс каждого игрока отображается в виде полоски под его именем.
|
||||
|
||||
## 🗺️ Локации и маршруты
|
||||
|
||||
Игра включает:
|
||||
- **48 городов, портов и хабов** по всему миру
|
||||
- **150+ путевых точек** на основных морских маршрутах
|
||||
- **Равномерные расстояния** - каждая точка примерно на одинаковом расстоянии
|
||||
- **Альтернативные маршруты** для выбора оптимального пути
|
||||
|
||||
### 🏙️ Основные города
|
||||
|
||||
**🇪🇺 Европа:** Лондон, Париж, Роттердам, Гамбург, Барселона, Рим, Афины, Стамбул, Москва, Санкт-Петербург, Мурманск
|
||||
|
||||
**🌍 Африка:** Каир, Суэц, Кейптаун, Лагос, Найроби
|
||||
|
||||
**🌏 Азия:** Мумбаи, Мундра, Ченнаи, Дели, Сингапур, Бангкок, Гонконг, Шанхай, Пекин, Тяньцзинь, Циндао, Токио, Сеул, Пусан, Дубай, Владивосток
|
||||
|
||||
**🌎 Северная Америка:** Нью-Йорк, Вашингтон, Чикаго, Лос-Анджелес, Ванкувер, Майами, Мехико
|
||||
|
||||
**🌎 Южная Америка:** Панама, Колон, Лима, Рио-де-Жанейро, Сантос, Буэнос-Айрес
|
||||
|
||||
**🦘 Океания:** Сидней, Мельбурн, Окленд
|
||||
|
||||
### 🌊 Морские маршруты
|
||||
|
||||
На карте отображены исторические торговые пути с равномерными расстояниями:
|
||||
|
||||
#### Ключевые проливы и каналы:
|
||||
- **Гибралтарский пролив** — ворота Средиземного моря
|
||||
- **Суэцкий канал** — короткий путь в Азию
|
||||
- **Панамский канал** — короткий путь между океанами
|
||||
- **Малаккский пролив** — ворота в Южно-Китайское море
|
||||
|
||||
#### Альтернативные маршруты:
|
||||
- **Мыс Доброй Надежды** — вокруг Африки (альтернатива Суэцу)
|
||||
- **Мыс Горн** — вокруг Южной Америки (альтернатива Панаме)
|
||||
|
||||
#### Основные океанские пути:
|
||||
- **Транс-Атлантические** (Нью-Йорк ⟷ Европа) - ~10 шагов
|
||||
- **Транс-Тихоокеанские** (Токио ⟷ Лос-Анджелес) - ~20 шагов
|
||||
- **Южная Атлантика** (Бразилия ⟷ Африка) - ~10 шагов
|
||||
- **Индийский океан (юг)** (Африка ⟷ Австралия) - ~15 шагов
|
||||
- **Индийский океан (центр)** (Дубай ⟷ Сингапур) - ~12 шагов
|
||||
|
||||
### 🎯 Путевые точки
|
||||
|
||||
Маленькие белые точки на карте — это промежуточные пункты на морских маршрутах. Игроки должны проходить через них, планируя свой путь между городами.
|
||||
|
||||
## 📦 Типы грузов
|
||||
|
||||
### Легкие грузы (можно взять 3-4 задания):
|
||||
- 📱 **Электроника** - 80 кг / 120 м³ → 500 очков
|
||||
- 👔 **Текстиль** - 60 кг / 100 м³ → 250 очков
|
||||
- 🍎 **Продукты** - 100 кг / 150 м³ → 300 очков
|
||||
|
||||
### Средние грузы (можно взять 2-3 задания):
|
||||
- 🧪 **Химикаты** - 150 кг / 180 м³ → 400 очков
|
||||
- 🪑 **Мебель** - 120 кг / 200 м³ → 320 очков
|
||||
|
||||
### Тяжелые грузы (можно взять 1-2 задания):
|
||||
- ⚙️ **Оборудование** - 200 кг / 250 м³ → 450 очков
|
||||
- 🔩 **Металлы** - 250 кг / 180 м³ → 380 очков
|
||||
- 🚗 **Автомобили** - 280 кг / 300 м³ → 600 очков
|
||||
|
||||
## 🚀 Запуск игры
|
||||
|
||||
1. Откройте файл **`index.html`** в любом современном браузере
|
||||
2. Выберите количество игроков
|
||||
3. Введите имена и начните игру!
|
||||
|
||||
## 🎲 Механика ходов
|
||||
|
||||
- Каждый ход игрок бросает **2 кости** (1-6 на каждой)
|
||||
- Сумма костей определяет, на сколько шагов может пойти игрок
|
||||
- Шаги считаются по маршрутам между локациями
|
||||
- Игрок может выбрать **любую доступную локацию** в пределах выпавших шагов
|
||||
- Необязательно использовать все шаги
|
||||
|
||||
## 🎓 Образовательная ценность
|
||||
|
||||
Игра помогает:
|
||||
- ✅ Запомнить расположение основных городов мира
|
||||
- ✅ Изучить географию континентов
|
||||
- ✅ Понять основные торговые пути
|
||||
- ✅ Развить стратегическое мышление
|
||||
- ✅ Улучшить знание стран и столиц
|
||||
|
||||
## 🛠️ Технологии
|
||||
|
||||
- **HTML5 Canvas** — для рендеринга карты и анимаций
|
||||
- **Vanilla JavaScript** — без фреймворков
|
||||
- **CSS3** — современный UI с градиентами и анимациями
|
||||
|
||||
## 🎨 Дизайн
|
||||
|
||||
- Реалистичная карта мира с континентами
|
||||
- Цветные фишки для игроков: 🔴 🔵 🟢 🟡
|
||||
- Интуитивный интерфейс
|
||||
- Подсветка доступных ходов
|
||||
- Анимация бросания костей
|
||||
|
||||
## 📱 Совместимость
|
||||
|
||||
Игра работает в любом современном браузере:
|
||||
- Chrome / Edge
|
||||
- Firefox
|
||||
- Safari
|
||||
- Opera
|
||||
|
||||
## 🎯 Стратегии
|
||||
|
||||
- **Планируйте маршруты** заранее, выбирая задания
|
||||
- **Изучайте карту** — запоминайте расположение локаций
|
||||
- **Выбирайте задания** с выгодными маршрутами
|
||||
- **Используйте хабы** для быстрого перемещения между континентами
|
||||
|
||||
## 🔮 Будущие обновления
|
||||
|
||||
- [ ] Различные типы транспорта (корабль, поезд, самолет)
|
||||
- [ ] Карточки событий (погода, задержки)
|
||||
- [ ] Система уровней сложности
|
||||
- [ ] Режим на время
|
||||
- [ ] Режим соревнования
|
||||
- [ ] Статистика и достижения
|
||||
- [ ] Сохранение игры
|
||||
|
||||
---
|
||||
|
||||
**Приятной игры и изучения географии! 🌍✈️🚢**
|
||||
679
data.js
Normal file
679
data.js
Normal file
|
|
@ -0,0 +1,679 @@
|
|||
// Данные о локациях на карте мира
|
||||
// Координаты рассчитаны для Equirectangular проекции
|
||||
// Формула: x = (lon + 180) * (width / 360), y = (90 - lat) * (height / 180)
|
||||
// Для canvas 1100x600
|
||||
|
||||
const locations = [
|
||||
// Европа
|
||||
{ id: 'london', name: 'Лондон', type: 'city', lat: 51.5074, lon: -0.1278, country: 'Великобритания', icon: '🏙️', continent: 'Европа' },
|
||||
{ id: 'paris', name: 'Париж', type: 'city', lat: 48.8566, lon: 2.3522, country: 'Франция', icon: '🏙️', continent: 'Европа' },
|
||||
{ id: 'rotterdam', name: 'Роттердам', type: 'port', lat: 51.9225, lon: 4.4792, country: 'Нидерланды', icon: '🚢', continent: 'Европа' },
|
||||
{ id: 'hamburg', name: 'Гамбург', type: 'port', lat: 53.5511, lon: 9.9937, country: 'Германия', icon: '🚢', continent: 'Европа' },
|
||||
{ id: 'barcelona', name: 'Барселона', type: 'port', lat: 41.3851, lon: 2.1734, country: 'Испания', icon: '🚢', continent: 'Европа' },
|
||||
{ id: 'rome', name: 'Рим', type: 'city', lat: 41.9028, lon: 12.4964, country: 'Италия', icon: '🏙️', continent: 'Европа' },
|
||||
{ id: 'athens', name: 'Афины', type: 'port', lat: 37.9838, lon: 23.7275, country: 'Греция', icon: '🚢', continent: 'Европа' },
|
||||
{ id: 'limassol', name: 'Лимассол', type: 'port', lat: 34.6751, lon: 33.0443, country: 'Кипр', icon: '🚢', continent: 'Европа' },
|
||||
{ id: 'istanbul', name: 'Стамбул', type: 'hub', lat: 41.0082, lon: 28.9784, country: 'Турция', icon: '🏭', continent: 'Европа/Азия' },
|
||||
{ id: 'moscow', name: 'Москва', type: 'hub', lat: 55.7558, lon: 37.6173, country: 'Россия', icon: '🏭', continent: 'Европа' },
|
||||
{ id: 'stpetersburg', name: 'Санкт-Петербург', type: 'port', lat: 59.9343, lon: 30.3351, country: 'Россия', icon: '🚢', continent: 'Европа' },
|
||||
{ id: 'murmansk', name: 'Мурманск', type: 'port', lat: 68.9667, lon: 33.0833, country: 'Россия', icon: '🚢', continent: 'Европа' },
|
||||
{ id: 'nuuk', name: 'Нуук', type: 'port', lat: 64.1814, lon: -51.6941, country: 'Гренландия', icon: '🚢', continent: 'Северная Америка' },
|
||||
{ id: 'sevastopol', name: 'Севастополь', type: 'port', lat: 44.6167, lon: 33.5250, country: 'Россия', icon: '🚢', continent: 'Европа' },
|
||||
|
||||
// Ближний Восток и Африка
|
||||
{ id: 'tangier', name: 'Танжер', type: 'port', lat: 35.7595, lon: -5.8335, country: 'Марокко', icon: '🚢', continent: 'Африка' },
|
||||
{ id: 'cairo', name: 'Каир', type: 'city', lat: 30.0444, lon: 31.2357, country: 'Египет', icon: '🏙️', continent: 'Африка' },
|
||||
{ id: 'dubai', name: 'Дубай', type: 'port', lat: 25.2048, lon: 55.2708, country: 'ОАЭ', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'capetown', name: 'Кейптаун', type: 'port', lat: -33.9249, lon: 18.4241, country: 'ЮАР', icon: '🚢', continent: 'Африка' },
|
||||
{ id: 'lagos', name: 'Лагос', type: 'port', lat: 6.5244, lon: 3.3792, country: 'Нигерия', icon: '🚢', continent: 'Африка' },
|
||||
{ id: 'toamasina', name: 'Туамасина', type: 'port', lat: -18.1492, lon: 49.4023, country: 'Мадагаскар', icon: '🚢', continent: 'Африка' },
|
||||
{ id: 'nairobi', name: 'Найроби', type: 'hub', lat: -1.2864, lon: 36.8172, country: 'Кения', icon: '🏭', continent: 'Африка' },
|
||||
|
||||
// Азия
|
||||
{ id: 'mumbai', name: 'Мумбаи', type: 'port', lat: 19.0760, lon: 72.8777, country: 'Индия', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'mundra', name: 'Мундра', type: 'port', lat: 22.75, lon: 69.7167, country: 'Индия', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'chennai', name: 'Ченнаи', type: 'port', lat: 13.0833, lon: 80.2833, country: 'Индия', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'delhi', name: 'Дели', type: 'hub', lat: 28.7041, lon: 77.1025, country: 'Индия', icon: '🏭', continent: 'Азия' },
|
||||
{ id: 'singapore', name: 'Сингапур', type: 'port', lat: 1.3521, lon: 103.8198, country: 'Сингапур', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'jakarta', name: 'Танджунг Приок (Джакарта)', type: 'port', lat: -6.1214, lon: 106.8814, country: 'Индонезия', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'belawan', name: 'Белаван (Медан)', type: 'port', lat: 3.7894, lon: 98.6977, country: 'Индонезия', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'bangkok', name: 'Бангкок', type: 'hub', lat: 13.7563, lon: 100.5018, country: 'Таиланд', icon: '🏭', continent: 'Азия' },
|
||||
{ id: 'hongkong', name: 'Гонконг', type: 'port', lat: 22.3193, lon: 114.1694, country: 'Китай', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'shanghai', name: 'Шанхай', type: 'port', lat: 31.2304, lon: 121.4737, country: 'Китай', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'beijing', name: 'Пекин', type: 'city', lat: 39.9042, lon: 116.4074, country: 'Китай', icon: '🏙️', continent: 'Азия' },
|
||||
{ id: 'tianjin', name: 'Тяньцзинь', type: 'port', lat: 38.9667, lon: 117.7833, country: 'Китай', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'qingdao', name: 'Циндао', type: 'port', lat: 36.0667, lon: 120.3167, country: 'Китай', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'tokyo', name: 'Токио', type: 'port', lat: 35.6762, lon: 139.6503, country: 'Япония', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'seoul', name: 'Сеул', type: 'hub', lat: 37.5665, lon: 126.9780, country: 'Южная Корея', icon: '🏭', continent: 'Азия' },
|
||||
{ id: 'busan', name: 'Пусан', type: 'port', lat: 35.1, lon: 129.0333, country: 'Южная Корея', icon: '🚢', continent: 'Азия' },
|
||||
{ id: 'vladivostok', name: 'Владивосток', type: 'port', lat: 43.1332, lon: 131.9113, country: 'Россия', icon: '🚢', continent: 'Азия' },
|
||||
|
||||
// Америка
|
||||
{ id: 'newyork', name: 'Нью-Йорк', type: 'port', lat: 40.7128, lon: -74.0060, country: 'США', icon: '🚢', continent: 'Северная Америка' },
|
||||
{ id: 'washington', name: 'Вашингтон', type: 'city', lat: 38.9072, lon: -77.0369, country: 'США', icon: '🏙️', continent: 'Северная Америка' },
|
||||
{ id: 'chicago', name: 'Чикаго', type: 'hub', lat: 41.8781, lon: -87.6298, country: 'США', icon: '🏭', continent: 'Северная Америка' },
|
||||
{ id: 'losangeles', name: 'Лос-Анджелес', type: 'port', lat: 34.0522, lon: -118.2437, country: 'США', icon: '🚢', continent: 'Северная Америка' },
|
||||
{ id: 'vancouver', name: 'Ванкувер', type: 'port', lat: 49.2833, lon: -123.1167, country: 'Канада', icon: '🚢', continent: 'Северная Америка' },
|
||||
{ id: 'miami', name: 'Майами', type: 'port', lat: 25.7617, lon: -80.1918, country: 'США', icon: '🚢', continent: 'Северная Америка' },
|
||||
{ id: 'havana', name: 'Гавана', type: 'port', lat: 23.1136, lon: -82.3666, country: 'Куба', icon: '🚢', continent: 'Северная Америка' },
|
||||
{ id: 'mexicocity', name: 'Мехико', type: 'hub', lat: 19.4326, lon: -99.1332, country: 'Мексика', icon: '🏭', continent: 'Северная Америка' },
|
||||
{ id: 'colon', name: 'Колон', type: 'port', lat: 9.35, lon: -79.9, country: 'Панама', icon: '🚢', continent: 'Центральная Америка' },
|
||||
{ id: 'paramaribo', name: 'Парамарибо', type: 'port', lat: 5.8520, lon: -55.2038, country: 'Суринам', icon: '🚢', continent: 'Южная Америка' },
|
||||
{ id: 'cumana', name: 'Кумана', type: 'port', lat: 10.4631, lon: -64.1814, country: 'Венесуэла', icon: '🚢', continent: 'Южная Америка' },
|
||||
{ id: 'lima', name: 'Лима', type: 'port', lat: -12.0464, lon: -77.0428, country: 'Перу', icon: '🚢', continent: 'Южная Америка' },
|
||||
{ id: 'puntarenas', name: 'Пунта-Аренас', type: 'port', lat: -53.1638, lon: -70.9171, country: 'Чили', icon: '🚢', continent: 'Южная Америка' },
|
||||
{ id: 'riodejaneiro', name: 'Рио-де-Жанейро', type: 'port', lat: -22.9068, lon: -43.1729, country: 'Бразилия', icon: '🚢', continent: 'Южная Америка' },
|
||||
{ id: 'santos', name: 'Сантос', type: 'port', lat: -23.9667, lon: -46.3167, country: 'Бразилия', icon: '🚢', continent: 'Южная Америка' },
|
||||
{ id: 'natal', name: 'Натал', type: 'port', lat: -5.7945, lon: -35.2119, country: 'Бразилия', icon: '🚢', continent: 'Южная Америка' },
|
||||
{ id: 'buenosaires', name: 'Буэнос-Айрес', type: 'port', lat: -34.6037, lon: -58.3816, country: 'Аргентина', icon: '🚢', continent: 'Южная Америка' },
|
||||
|
||||
// Океания
|
||||
{ id: 'sydney', name: 'Сидней', type: 'port', lat: -33.8688, lon: 151.2093, country: 'Австралия', icon: '🚢', continent: 'Океания' },
|
||||
{ id: 'melbourne', name: 'Мельбурн', type: 'port', lat: -37.8136, lon: 144.9631, country: 'Австралия', icon: '🚢', continent: 'Океания' },
|
||||
{ id: 'auckland', name: 'Окленд', type: 'port', lat: -36.8485, lon: 174.7633, country: 'Новая Зеландия', icon: '🚢', continent: 'Океания' },
|
||||
];
|
||||
|
||||
// Координаты теперь рассчитываются динамически в map.js на основе lat/lon
|
||||
|
||||
// Путевые точки на основных морских маршрутах (waypoints)
|
||||
// Создаем плотную сетку точек для равномерного перемещения
|
||||
const waypoints = [
|
||||
// Транс-Атлантика: Нью-Йорк - Европа (убрали слишком близкие точки)
|
||||
{ id: 'atl_2', lat: 44, lon: -60, name: 'Атлантика (44°N, 60°W)' },
|
||||
{ id: 'atl_3', lat: 46, lon: -50, name: 'Атлантика (46°N, 50°W)' },
|
||||
{ id: 'atl_4', lat: 48, lon: -40, name: 'Атлантика (48°N, 40°W)' },
|
||||
{ id: 'atl_5', lat: 50, lon: -30, name: 'Атлантика (50°N, 30°W)' },
|
||||
{ id: 'atl_6', lat: 51, lon: -20, name: 'Атлантика (51°N, 20°W)' },
|
||||
|
||||
// Восточное побережье США (убрали слишком близкие)
|
||||
{ id: 'us_east_2', lat: 33, lon: -78, name: 'Вост. побережье (33°N, 78°W)' },
|
||||
|
||||
// Внутренние маршруты через США (между Чикаго и Лос-Анджелесом)
|
||||
{ id: 'us_inland_1', lat: 40, lon: -100, name: 'Средний Запад (40°N, 100°W)' },
|
||||
{ id: 'us_inland_2', lat: 38, lon: -105, name: 'Средний Запад (38°N, 105°W)' },
|
||||
{ id: 'us_inland_3', lat: 36, lon: -110, name: 'Юго-Запад (36°N, 110°W)' },
|
||||
{ id: 'us_inland_4', lat: 35, lon: -115, name: 'Юго-Запад (35°N, 115°W)' },
|
||||
|
||||
// Северная Атлантика - между Майами и центральной Атлантикой
|
||||
{ id: 'atl_n_1', lat: 26, lon: -75, name: 'Север. Атлантика (26°N, 75°W)' },
|
||||
{ id: 'atl_n_2', lat: 28, lon: -70, name: 'Север. Атлантика (28°N, 70°W)' },
|
||||
{ id: 'atl_n_3', lat: 30, lon: -65, name: 'Север. Атлантика (30°N, 65°W)' },
|
||||
{ id: 'atl_n_4', lat: 28, lon: -55, name: 'Север. Атлантика (28°N, 55°W)' },
|
||||
{ id: 'atl_n_5', lat: 25, lon: -45, name: 'Север. Атлантика (25°N, 45°W)' },
|
||||
{ id: 'atl_n_6', lat: 22, lon: -38, name: 'Север. Атлантика (22°N, 38°W)' },
|
||||
{ id: 'atl_n_7', lat: 19, lon: -30, name: 'Север. Атлантика (19°N, 30°W)' },
|
||||
{ id: 'atl_n_8', lat: 17, lon: -25, name: 'Север. Атлантика (17°N, 25°W)' },
|
||||
|
||||
// Карибское море (плотная сетка)
|
||||
{ id: 'car_1', lat: 23, lon: -75, name: 'Карибское море (23°N, 75°W)' },
|
||||
{ id: 'car_2', lat: 20, lon: -75, name: 'Карибское море (20°N, 75°W)' },
|
||||
{ id: 'car_3', lat: 18, lon: -78, name: 'Карибское море (18°N, 78°W)' },
|
||||
{ id: 'car_4', lat: 16, lon: -82, name: 'Карибское море (16°N, 82°W)' },
|
||||
{ id: 'car_5', lat: 14, lon: -80, name: 'Карибское море (14°N, 80°W)' },
|
||||
{ id: 'car_6', lat: 12, lon: -75, name: 'Карибское море (12°N, 75°W)' },
|
||||
{ id: 'car_7', lat: 15, lon: -70, name: 'Карибское море (15°N, 70°W)' },
|
||||
|
||||
// Панамский канал и подходы
|
||||
{ id: 'panama_w', lat: 10, lon: -82, name: 'Подход к Панаме' },
|
||||
{ id: 'panama_canal', lat: 9, lon: -79.5, name: 'Панамский канал' },
|
||||
{ id: 'panama_e', lat: 8, lon: -77, name: 'Тихий океан' },
|
||||
|
||||
// Тихий океан - Восточная часть (США)
|
||||
{ id: 'pac_e_1', lat: 36, lon: -122, name: 'Тихий океан (36°N, 122°W)' },
|
||||
{ id: 'pac_e_2', lat: 32, lon: -118, name: 'Тихий океан (32°N, 118°W)' },
|
||||
{ id: 'pac_e_3', lat: 28, lon: -115, name: 'Тихий океан (28°N, 115°W)' },
|
||||
{ id: 'pac_e_4', lat: 24, lon: -112, name: 'Тихий океан (24°N, 112°W)' },
|
||||
{ id: 'pac_e_5', lat: 20, lon: -110, name: 'Тихий океан (20°N, 110°W)' },
|
||||
{ id: 'pac_e_6', lat: 16, lon: -105, name: 'Тихий океан (16°N, 105°W)' },
|
||||
{ id: 'pac_e_7', lat: 12, lon: -95, name: 'Тихий океан (12°N, 95°W)' },
|
||||
{ id: 'pac_e_8', lat: 8, lon: -90, name: 'Тихий океан (8°N, 90°W)' },
|
||||
{ id: 'pac_e_9', lat: 4, lon: -85, name: 'Тихий океан (4°N, 85°W)' },
|
||||
|
||||
// Южная Америка - Тихоокеанское побережье
|
||||
{ id: 'sa_pac_1', lat: 0, lon: -82, name: 'Тихий океан (0°, 82°W)' },
|
||||
{ id: 'sa_pac_2', lat: -4, lon: -80, name: 'Тихий океан (4°S, 80°W)' },
|
||||
{ id: 'sa_pac_3', lat: -8, lon: -79, name: 'Тихий океан (8°S, 79°W)' },
|
||||
{ id: 'sa_pac_4', lat: -12, lon: -78, name: 'Тихий океан (12°S, 78°W)' },
|
||||
{ id: 'sa_pac_5', lat: -16, lon: -76, name: 'Тихий океан (16°S, 76°W)' },
|
||||
{ id: 'sa_pac_6', lat: -20, lon: -72, name: 'Тихий океан (20°S, 72°W)' },
|
||||
{ id: 'sa_pac_7', lat: -25, lon: -71, name: 'Тихий океан (25°S, 71°W)' },
|
||||
{ id: 'sa_pac_8', lat: -30, lon: -72, name: 'Тихий океан (30°S, 72°W)' },
|
||||
{ id: 'sa_pac_9', lat: -35, lon: -73, name: 'Тихий океан (35°S, 73°W)' },
|
||||
{ id: 'sa_pac_10', lat: -40, lon: -74, name: 'Тихий океан (40°S, 74°W)' },
|
||||
{ id: 'sa_pac_11', lat: -45, lon: -75, name: 'Тихий океан (45°S, 75°W)' },
|
||||
{ id: 'sa_pac_12', lat: -50, lon: -76, name: 'Тихий океан (50°S, 76°W)' },
|
||||
|
||||
// Мыс Горн
|
||||
{ id: 'horn_1', lat: -54, lon: -70, name: 'Мыс Горн' },
|
||||
{ id: 'horn_2', lat: -56, lon: -68, name: 'Мыс Горн' },
|
||||
{ id: 'horn_3', lat: -55, lon: -65, name: 'Мыс Горн' },
|
||||
|
||||
// Южная Америка - Атлантическое побережье (добавлено больше точек)
|
||||
{ id: 'sa_atl_1', lat: -50, lon: -62, name: 'Атлантика (50°S, 62°W)' },
|
||||
{ id: 'sa_atl_2', lat: -45, lon: -60, name: 'Атлантика (45°S, 60°W)' },
|
||||
{ id: 'sa_atl_3', lat: -40, lon: -58, name: 'Атлантика (40°S, 58°W)' },
|
||||
{ id: 'sa_atl_4', lat: -35, lon: -56, name: 'Атлантика (35°S, 56°W)' },
|
||||
{ id: 'sa_atl_5', lat: -30, lon: -52, name: 'Атлантика (30°S, 52°W)' },
|
||||
{ id: 'sa_atl_6', lat: -25, lon: -48, name: 'Атлантика (25°S, 48°W)' },
|
||||
{ id: 'sa_atl_7', lat: -20, lon: -44, name: 'Атлантика (20°S, 44°W)' },
|
||||
{ id: 'sa_atl_8', lat: -15, lon: -40, name: 'Атлантика (15°S, 40°W)' },
|
||||
{ id: 'sa_atl_9', lat: -10, lon: -36, name: 'Атлантика (10°S, 36°W)' },
|
||||
{ id: 'sa_atl_10', lat: -5, lon: -32, name: 'Атлантика (5°S, 32°W)' },
|
||||
|
||||
// Северное побережье Южной Америки (от Парамарибо к Наталу)
|
||||
{ id: 'sa_north_1', lat: 2, lon: -50, name: 'Сев. побережье Ю.Америки (2°N, 50°W)' },
|
||||
{ id: 'sa_north_2', lat: 0, lon: -45, name: 'Сев. побережье Ю.Америки (0°, 45°W)' },
|
||||
{ id: 'sa_north_3', lat: -2, lon: -40, name: 'Сев. побережье Ю.Америки (2°S, 40°W)' },
|
||||
|
||||
// Южная Атлантика - между Ю.Америкой и Африкой (МНОГО точек)
|
||||
{ id: 's_atl_1', lat: -5, lon: -28, name: 'Южн. Атлантика 16' },
|
||||
{ id: 's_atl_2', lat: -5, lon: -24, name: 'Южн. Атлантика 15' },
|
||||
{ id: 's_atl_3', lat: -5, lon: -20, name: 'Южн. Атлантика 14' },
|
||||
{ id: 's_atl_4', lat: -5, lon: -16, name: 'Южн. Атлантика 13' },
|
||||
{ id: 's_atl_5', lat: -5, lon: -12, name: 'Южн. Атлантика 12' },
|
||||
{ id: 's_atl_6', lat: -5, lon: -8, name: 'Южн. Атлантика 11' },
|
||||
|
||||
// Центральная Атлантика
|
||||
{ id: 'atl_mid_1', lat: 0, lon: -28, name: 'Атлантика (0°, 28°W)' },
|
||||
{ id: 'atl_mid_2', lat: 5, lon: -25, name: 'Атлантика (5°N, 25°W)' },
|
||||
{ id: 'atl_mid_3', lat: 10, lon: -22, name: 'Атлантика (10°N, 22°W)' },
|
||||
{ id: 'atl_mid_4', lat: 15, lon: -20, name: 'Атлантика (15°N, 20°W)' },
|
||||
{ id: 'atl_mid_5', lat: 20, lon: -18, name: 'Атлантика (20°N, 18°W)' },
|
||||
{ id: 'atl_mid_6', lat: 25, lon: -16, name: 'Атлантика (25°N, 16°W)' },
|
||||
{ id: 'atl_mid_7', lat: 30, lon: -14, name: 'Атлантика (30°N, 14°W)' },
|
||||
|
||||
// Западная Африка
|
||||
{ id: 'waf_1', lat: 33, lon: -10, name: 'Африка' },
|
||||
{ id: 'waf_2', lat: 28, lon: -12, name: 'Африка' },
|
||||
{ id: 'waf_3', lat: 23, lon: -15, name: 'Африка' },
|
||||
{ id: 'waf_4', lat: 18, lon: -17, name: 'Африка' },
|
||||
{ id: 'waf_5', lat: 13, lon: -18, name: 'Африка' },
|
||||
{ id: 'waf_6', lat: 8, lon: -15, name: 'Африка' },
|
||||
{ id: 'waf_7', lat: 3, lon: -10, name: 'Африка' },
|
||||
{ id: 'waf_8', lat: -2, lon: -5, name: 'Африка' },
|
||||
{ id: 'waf_9', lat: -7, lon: 0, name: 'Африка' },
|
||||
{ id: 'waf_10', lat: -12, lon: 5, name: 'Африка' },
|
||||
{ id: 'waf_11', lat: -17, lon: 8, name: 'Африка' },
|
||||
{ id: 'waf_12', lat: -22, lon: 10, name: 'Африка' },
|
||||
{ id: 'waf_13', lat: -27, lon: 13, name: 'Африка' },
|
||||
{ id: 'waf_14', lat: -32, lon: 16, name: 'Африка' },
|
||||
|
||||
// Мыс Доброй Надежды
|
||||
{ id: 'cape_1', lat: -35, lon: 18, name: 'Мыс Доброй Надежды' },
|
||||
{ id: 'cape_2', lat: -36, lon: 20, name: 'Мыс Доброй Надежды' },
|
||||
{ id: 'cape_3', lat: -35, lon: 23, name: 'Мыс Доброй Надежды' },
|
||||
|
||||
// Восточная Африка
|
||||
{ id: 'eaf_1', lat: -32, lon: 28, name: 'Африка' },
|
||||
{ id: 'eaf_2', lat: -27, lon: 32, name: 'Африка' },
|
||||
{ id: 'eaf_3', lat: -22, lon: 35, name: 'Африка' },
|
||||
{ id: 'eaf_4', lat: -17, lon: 38, name: 'Африка' },
|
||||
{ id: 'eaf_5', lat: -12, lon: 40, name: 'Африка' },
|
||||
{ id: 'eaf_6', lat: -7, lon: 42, name: 'Африка' },
|
||||
{ id: 'eaf_7', lat: -2, lon: 43, name: 'Африка' },
|
||||
{ id: 'eaf_8', lat: 3, lon: 42, name: 'Африка' },
|
||||
{ id: 'eaf_9', lat: 8, lon: 45, name: 'Африка' },
|
||||
{ id: 'eaf_10', lat: 13, lon: 48, name: 'Африка' },
|
||||
|
||||
// К Мадагаскару от восточного побережья Африки
|
||||
{ id: 'mad_1', lat: -8, lon: 45, name: 'Мозамбикский пролив' },
|
||||
{ id: 'mad_2', lat: -12, lon: 46, name: 'Мозамбикский пролив' },
|
||||
{ id: 'mad_3', lat: -16, lon: 47.5, name: 'Мозамбикский пролив' },
|
||||
|
||||
// Красное море (без Суэца)
|
||||
{ id: 'red_1', lat: 17, lon: 40, name: 'Красное море' },
|
||||
{ id: 'red_2', lat: 20, lon: 38, name: 'Красное море' },
|
||||
{ id: 'red_3', lat: 23, lon: 36, name: 'Красное море' },
|
||||
{ id: 'red_4', lat: 26, lon: 34, name: 'Красное море' },
|
||||
|
||||
// Черное море
|
||||
{ id: 'black_1', lat: 42.5, lon: 31, name: 'Черное море' },
|
||||
|
||||
// Средиземное море
|
||||
{ id: 'med_1', lat: 32, lon: 28, name: 'Средиземное море' },
|
||||
{ id: 'med_2', lat: 34, lon: 24, name: 'Средиземное море' },
|
||||
{ id: 'med_3', lat: 35, lon: 20, name: 'Средиземное море' },
|
||||
{ id: 'med_4', lat: 36, lon: 16, name: 'Средиземное море' },
|
||||
{ id: 'med_5', lat: 38, lon: 12, name: 'Средиземное море' },
|
||||
{ id: 'med_6', lat: 39, lon: 8, name: 'Средиземное море' },
|
||||
{ id: 'med_7', lat: 40, lon: 4, name: 'Средиземное море' },
|
||||
|
||||
// Восточное Средиземное море (к Кипру)
|
||||
{ id: 'med_e_1', lat: 34, lon: 30, name: 'Восточ. Средиземноморье' },
|
||||
{ id: 'med_e_2', lat: 35, lon: 28, name: 'Восточ. Средиземноморье' },
|
||||
{ id: 'gibraltar', lat: 36, lon: -5.5, name: 'Гибралтар' },
|
||||
|
||||
// Северная Европа
|
||||
{ id: 'neur_1', lat: 45, lon: -4, name: 'Атлантика' },
|
||||
{ id: 'neur_2', lat: 48, lon: -2, name: 'Атлантика' },
|
||||
{ id: 'neur_3', lat: 50, lon: 0, name: 'Атлантика' },
|
||||
{ id: 'neur_4', lat: 52, lon: 2, name: 'Северное море' },
|
||||
{ id: 'neur_5', lat: 54, lon: 5, name: 'Северное море' },
|
||||
|
||||
// Северный морской путь (к Мурманску)
|
||||
{ id: 'north_1', lat: 62, lon: 10, name: 'Северное море' },
|
||||
{ id: 'north_2', lat: 65, lon: 18, name: 'Баренцево море' },
|
||||
{ id: 'north_3', lat: 67, lon: 25, name: 'Баренцево море' },
|
||||
|
||||
// К Гренландии (Северная Атлантика)
|
||||
{ id: 'greenland_1', lat: 60, lon: -25, name: 'Северная Атлантика' },
|
||||
{ id: 'greenland_2', lat: 62, lon: -30, name: 'Северная Атлантика' },
|
||||
{ id: 'greenland_3', lat: 63, lon: -40, name: 'Северная Атлантика' },
|
||||
|
||||
// Индийский океан (расширенный)
|
||||
{ id: 'ind_1', lat: 15, lon: 55, name: 'Индийский океан' },
|
||||
{ id: 'ind_2', lat: 12, lon: 60, name: 'Индийский океан' },
|
||||
{ id: 'ind_3', lat: 10, lon: 65, name: 'Индийский океан' },
|
||||
{ id: 'ind_4', lat: 8, lon: 70, name: 'Индийский океан' },
|
||||
{ id: 'ind_5', lat: 5, lon: 75, name: 'Индийский океан' },
|
||||
{ id: 'ind_6', lat: 2, lon: 80, name: 'Индийский океан' },
|
||||
{ id: 'ind_7', lat: 0, lon: 85, name: 'Индийский океан' },
|
||||
{ id: 'ind_8', lat: -2, lon: 90, name: 'Индийский океан' },
|
||||
{ id: 'ind_9', lat: -3, lon: 95, name: 'Индийский океан' },
|
||||
|
||||
// Индийский океан - к Австралии (новый маршрут от Африки)
|
||||
{ id: 'ind_s_1', lat: -10, lon: 50, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_2', lat: -15, lon: 55, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_3', lat: -20, lon: 60, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_4', lat: -25, lon: 65, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_5', lat: -28, lon: 70, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_6', lat: -30, lon: 75, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_7', lat: -32, lon: 80, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_8', lat: -33, lon: 90, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_9', lat: -33, lon: 100, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_10', lat: -32, lon: 110, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_11', lat: -30, lon: 120, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_12', lat: -28, lon: 130, name: 'Индийский океан' },
|
||||
{ id: 'ind_s_13', lat: -26, lon: 140, name: 'Индийский океан' },
|
||||
|
||||
// Малаккский пролив
|
||||
{ id: 'malacca_1', lat: 6, lon: 98, name: 'Малаккский пролив' },
|
||||
{ id: 'malacca_2', lat: 4, lon: 100, name: 'Малаккский пролив' },
|
||||
{ id: 'malacca_3', lat: 2, lon: 102, name: 'Малаккский пролив' },
|
||||
|
||||
// Яванское море (между Джакартой и Сингапуром)
|
||||
{ id: 'java_1', lat: -2, lon: 105, name: 'Яванское море' },
|
||||
{ id: 'java_2', lat: 0, lon: 104, name: 'Яванское море' },
|
||||
|
||||
// Южно-Китайское море
|
||||
{ id: 'scs_1', lat: 4, lon: 106, name: 'Южно-Китайское море' },
|
||||
{ id: 'scs_2', lat: 7, lon: 109, name: 'Южно-Китайское море' },
|
||||
{ id: 'scs_3', lat: 10, lon: 112, name: 'Южно-Китайское море' },
|
||||
{ id: 'scs_4', lat: 13, lon: 114, name: 'Южно-Китайское море' },
|
||||
{ id: 'scs_5', lat: 16, lon: 116, name: 'Южно-Китайское море' },
|
||||
{ id: 'scs_6', lat: 19, lon: 117, name: 'Южно-Китайское море' },
|
||||
{ id: 'scs_7', lat: 22, lon: 118, name: 'Южно-Китайское море' },
|
||||
{ id: 'scs_8', lat: 25, lon: 120, name: 'Восточно-Китайское море' },
|
||||
{ id: 'scs_9', lat: 28, lon: 122, name: 'Восточно-Китайское море' },
|
||||
{ id: 'scs_10', lat: 31, lon: 125, name: 'Восточно-Китайское море' },
|
||||
|
||||
// К Японии
|
||||
{ id: 'jap_1', lat: 33, lon: 130, name: 'Японское море' },
|
||||
{ id: 'jap_2', lat: 35, lon: 135, name: 'Тихий океан' },
|
||||
|
||||
// Тихий океан - Западная часть
|
||||
{ id: 'pac_w_1', lat: 30, lon: 145, name: 'Тихий океан' },
|
||||
{ id: 'pac_w_2', lat: 25, lon: 150, name: 'Тихий океан' },
|
||||
{ id: 'pac_w_3', lat: 20, lon: 155, name: 'Тихий океан' },
|
||||
{ id: 'pac_w_4', lat: 15, lon: 160, name: 'Тихий океан' },
|
||||
{ id: 'pac_w_5', lat: 10, lon: 165, name: 'Тихий океан' },
|
||||
{ id: 'pac_w_6', lat: 5, lon: 170, name: 'Тихий океан' },
|
||||
{ id: 'pac_w_7', lat: 0, lon: 175, name: 'Тихий океан' },
|
||||
{ id: 'pac_w_8', lat: -5, lon: 178, name: 'Тихий океан' },
|
||||
|
||||
// К Австралии
|
||||
{ id: 'aus_1', lat: -10, lon: 142, name: 'Коралловое море' },
|
||||
{ id: 'aus_2', lat: -15, lon: 145, name: 'Коралловое море' },
|
||||
{ id: 'aus_3', lat: -20, lon: 148, name: 'Коралловое море' },
|
||||
{ id: 'aus_4', lat: -25, lon: 150, name: 'Коралловое море' },
|
||||
{ id: 'aus_5', lat: -30, lon: 152, name: 'Тасманово море' },
|
||||
{ id: 'aus_6', lat: -35, lon: 155, name: 'Тасманово море' },
|
||||
{ id: 'aus_7', lat: -37, lon: 160, name: 'Тасманово море' },
|
||||
|
||||
// К Новой Зеландии
|
||||
{ id: 'nz_1', lat: -38, lon: 168, name: 'Тасманово море' },
|
||||
{ id: 'nz_2', lat: -37, lon: 172, name: 'Тасманово море' },
|
||||
|
||||
// Транс-Тихоокеанский маршрут
|
||||
{ id: 'pac_c_1', lat: 35, lon: 160, name: 'Тихий океан' },
|
||||
{ id: 'pac_c_2', lat: 35, lon: 170, name: 'Тихий океан' },
|
||||
{ id: 'pac_c_3', lat: 35, lon: 180, name: 'Тихий океан' },
|
||||
{ id: 'pac_c_4', lat: 35, lon: -170, name: 'Тихий океан' },
|
||||
{ id: 'pac_c_5', lat: 35, lon: -160, name: 'Тихий океан' },
|
||||
{ id: 'pac_c_6', lat: 35, lon: -150, name: 'Тихий океан' },
|
||||
{ id: 'pac_c_7', lat: 35, lon: -140, name: 'Тихий океан' },
|
||||
{ id: 'pac_c_8', lat: 35, lon: -130, name: 'Тихий океан' },
|
||||
];
|
||||
|
||||
// Объединяем города и путевые точки
|
||||
const allPoints = [...locations, ...waypoints];
|
||||
|
||||
// Функция для создания последовательных маршрутов (двунаправленные)
|
||||
function createSequentialRoutes(ids) {
|
||||
const routes = [];
|
||||
for (let i = 0; i < ids.length - 1; i++) {
|
||||
// Прямой маршрут
|
||||
routes.push({ from: ids[i], to: ids[i + 1] });
|
||||
// Обратный маршрут
|
||||
routes.push({ from: ids[i + 1], to: ids[i] });
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
// Функция для создания одностороннего маршрута
|
||||
function createRoute(from, to) {
|
||||
return [
|
||||
{ from: from, to: to },
|
||||
{ from: to, to: from }
|
||||
];
|
||||
}
|
||||
|
||||
// Маршруты между точками (основные морские пути)
|
||||
const routes = [
|
||||
// Города Европы - внутренние связи (двунаправленные)
|
||||
...createRoute('london', 'rotterdam'),
|
||||
...createRoute('london', 'paris'),
|
||||
...createRoute('rotterdam', 'hamburg'),
|
||||
...createRoute('hamburg', 'stpetersburg'),
|
||||
...createRoute('stpetersburg', 'moscow'),
|
||||
...createRoute('moscow', 'sevastopol'),
|
||||
...createRoute('stpetersburg', 'murmansk'),
|
||||
...createRoute('paris', 'barcelona'),
|
||||
...createRoute('barcelona', 'tangier'),
|
||||
...createRoute('barcelona', 'rome'),
|
||||
...createRoute('rome', 'athens'),
|
||||
...createRoute('athens', 'istanbul'),
|
||||
|
||||
// Америка
|
||||
...createRoute('newyork', 'washington'),
|
||||
...createRoute('newyork', 'chicago'),
|
||||
// Чикаго - Лос-Анджелес через промежуточные точки (длинный маршрут)
|
||||
...createSequentialRoutes(['chicago', 'us_inland_1', 'us_inland_2', 'us_inland_3', 'us_inland_4', 'losangeles']),
|
||||
...createRoute('losangeles', 'vancouver'),
|
||||
...createRoute('vancouver', 'pac_e_1'),
|
||||
...createRoute('colon', 'panama_canal'),
|
||||
|
||||
// Азия - Китай
|
||||
...createRoute('beijing', 'tianjin'),
|
||||
...createRoute('tianjin', 'shanghai'),
|
||||
...createRoute('shanghai', 'qingdao'),
|
||||
...createRoute('qingdao', 'scs_10'),
|
||||
|
||||
// Азия - Корея и Япония
|
||||
...createRoute('seoul', 'busan'),
|
||||
...createRoute('busan', 'jap_1'),
|
||||
...createRoute('busan', 'tokyo'),
|
||||
...createRoute('tokyo', 'seoul'),
|
||||
...createRoute('seoul', 'vladivostok'),
|
||||
...createRoute('vladivostok', 'tokyo'),
|
||||
|
||||
// Азия - Индия
|
||||
...createRoute('mumbai', 'mundra'),
|
||||
...createRoute('mundra', 'mumbai'),
|
||||
...createRoute('mumbai', 'delhi'),
|
||||
...createRoute('mumbai', 'chennai'),
|
||||
...createRoute('chennai', 'delhi'),
|
||||
...createRoute('chennai', 'ind_6'),
|
||||
|
||||
// Южная Америка
|
||||
...createRoute('riodejaneiro', 'santos'),
|
||||
...createRoute('santos', 'buenosaires'),
|
||||
// Прямой маршрут Лима -> Рио удален, должен проходить через мыс Горн
|
||||
...createRoute('santos', 'sa_atl_8'),
|
||||
|
||||
// Океания
|
||||
...createRoute('sydney', 'melbourne'),
|
||||
|
||||
// Транс-Атлантика: Нью-Йорк - Европа (цепочка)
|
||||
...createSequentialRoutes(['newyork', 'atl_2', 'atl_3', 'atl_4', 'atl_5', 'atl_6', 'neur_3', 'london']),
|
||||
...createSequentialRoutes(['neur_3', 'neur_4', 'neur_5', 'hamburg']),
|
||||
...createSequentialRoutes(['hamburg', 'stpetersburg']),
|
||||
...createSequentialRoutes(['stpetersburg', 'north_1', 'north_2', 'north_3', 'murmansk']),
|
||||
...createSequentialRoutes(['newyork', 'atl_2', 'atl_3', 'atl_4', 'atl_5', 'atl_6', 'rotterdam']),
|
||||
|
||||
// Гренландия - Северная Атлантика
|
||||
...createSequentialRoutes(['nuuk', 'greenland_1', 'greenland_2', 'greenland_3', 'atl_5', 'atl_6']),
|
||||
...createSequentialRoutes(['nuuk', 'greenland_1', 'neur_3']),
|
||||
|
||||
// Восточное побережье США
|
||||
...createRoute('newyork', 'washington'),
|
||||
...createSequentialRoutes(['washington', 'us_east_2', 'miami']),
|
||||
// Вашингтон - Северная Атлантика (середина)
|
||||
...createSequentialRoutes(['washington', 'us_east_2', 'atl_n_3', 'atl_n_4', 'atl_n_5', 'atl_n_6']),
|
||||
|
||||
// Карибское море
|
||||
...createSequentialRoutes(['miami', 'car_1', 'car_2', 'car_3', 'car_4', 'car_5', 'panama_w', 'panama_canal']),
|
||||
...createSequentialRoutes(['miami', 'car_1', 'car_7', 'car_6']),
|
||||
...createSequentialRoutes(['panama_canal', 'colon']),
|
||||
// Гавана (Куба)
|
||||
...createSequentialRoutes(['miami', 'car_1', 'havana']),
|
||||
...createSequentialRoutes(['havana', 'car_2', 'car_3']),
|
||||
// Кумана (Венесуэла)
|
||||
...createSequentialRoutes(['cumana', 'car_6', 'car_7', 'car_1']),
|
||||
// Кумана - Натал (северное побережье Южной Америки)
|
||||
...createSequentialRoutes(['cumana', 'sa_north_1', 'sa_north_2', 'sa_north_3', 'sa_atl_10', 'sa_atl_9', 'natal']),
|
||||
// Парамарибо (Суринам)
|
||||
// Прямой маршрут paramaribo <-> car_6 удален (слишком короткий путь)
|
||||
// Парамарибо - Натал (северное побережье Южной Америки)
|
||||
...createSequentialRoutes(['paramaribo', 'sa_north_1', 'sa_north_2', 'sa_north_3', 'sa_atl_10', 'sa_atl_9', 'natal']),
|
||||
|
||||
// Тихий океан - восточная часть (США)
|
||||
...createSequentialRoutes(['losangeles', 'pac_e_1', 'pac_e_2', 'pac_e_3', 'pac_e_4', 'pac_e_5', 'pac_e_6', 'pac_e_7', 'pac_e_8', 'pac_e_9', 'panama_canal']),
|
||||
|
||||
// Панама - Южная Америка (Тихий океан)
|
||||
...createSequentialRoutes(['panama_canal', 'panama_e', 'sa_pac_1', 'sa_pac_2', 'sa_pac_3', 'sa_pac_4', 'lima']),
|
||||
...createSequentialRoutes(['lima', 'sa_pac_5', 'sa_pac_6', 'sa_pac_7', 'sa_pac_8', 'sa_pac_9', 'sa_pac_10', 'sa_pac_11', 'sa_pac_12']),
|
||||
|
||||
// Мыс Горн
|
||||
...createSequentialRoutes(['sa_pac_12', 'horn_1', 'horn_2', 'horn_3', 'sa_atl_1']),
|
||||
// Пунта-Аренас (Чили) - на юге
|
||||
...createSequentialRoutes(['puntarenas', 'horn_1', 'horn_2', 'horn_3']),
|
||||
...createRoute('puntarenas', 'sa_pac_12'),
|
||||
|
||||
// Южная Америка - Атлантика
|
||||
...createSequentialRoutes(['sa_atl_1', 'sa_atl_2', 'sa_atl_3', 'buenosaires']),
|
||||
...createSequentialRoutes(['buenosaires', 'sa_atl_4', 'sa_atl_5', 'sa_atl_6', 'sa_atl_7', 'riodejaneiro']),
|
||||
...createSequentialRoutes(['riodejaneiro', 'sa_atl_8', 'sa_atl_9', 'sa_atl_10']),
|
||||
// Натал (Бразилия) - северо-восточное побережье
|
||||
...createSequentialRoutes(['natal', 'sa_atl_8', 'sa_atl_9']),
|
||||
...createRoute('natal', 'riodejaneiro'),
|
||||
|
||||
// Южная Атлантика - переход к Африке (через середину океана)
|
||||
...createSequentialRoutes(['sa_atl_10', 's_atl_1', 's_atl_2', 's_atl_3', 's_atl_4', 's_atl_5', 's_atl_6', 'waf_8']),
|
||||
|
||||
// Центральная Атлантика (к северу)
|
||||
...createSequentialRoutes(['sa_atl_10', 'atl_mid_1', 'atl_mid_2', 'atl_mid_3', 'atl_mid_4', 'atl_mid_5', 'atl_mid_6', 'atl_mid_7', 'waf_1']),
|
||||
// Майами - Северная Атлантика (длинный маршрут)
|
||||
...createSequentialRoutes(['miami', 'atl_n_1', 'atl_n_2', 'atl_n_3', 'atl_n_4', 'atl_n_5', 'atl_n_6', 'atl_n_7', 'atl_n_8', 'atl_mid_4']),
|
||||
|
||||
// Связи между маршрутами Нью-Йорка и Северной Атлантики (посередине)
|
||||
...createRoute('atl_4', 'atl_n_5'),
|
||||
...createRoute('atl_5', 'atl_n_6'),
|
||||
...createRoute('atl_n_4', 'atl_4'),
|
||||
...createRoute('atl_n_5', 'atl_5'),
|
||||
|
||||
// Западная Африка
|
||||
...createSequentialRoutes(['waf_1', 'gibraltar']),
|
||||
...createSequentialRoutes(['waf_1', 'waf_2', 'waf_3', 'waf_4', 'waf_5', 'waf_6', 'lagos']),
|
||||
...createSequentialRoutes(['lagos', 'waf_7', 'waf_8', 'waf_9', 'waf_10', 'waf_11', 'waf_12', 'waf_13', 'waf_14']),
|
||||
|
||||
// Мыс Доброй Надежды
|
||||
...createSequentialRoutes(['waf_14', 'cape_1', 'capetown', 'cape_2', 'cape_3']),
|
||||
|
||||
// Восточная Африка
|
||||
...createSequentialRoutes(['cape_3', 'eaf_1', 'eaf_2', 'eaf_3', 'eaf_4', 'eaf_5', 'eaf_6', 'nairobi', 'eaf_7', 'eaf_8', 'eaf_9', 'eaf_10']),
|
||||
|
||||
// Красное море (без Суэца)
|
||||
...createSequentialRoutes(['eaf_10', 'red_1', 'red_2', 'red_3', 'red_4', 'cairo']),
|
||||
|
||||
// Средиземное море (без Суэца)
|
||||
...createSequentialRoutes(['cairo', 'med_1', 'med_2', 'athens']),
|
||||
...createSequentialRoutes(['med_2', 'med_3', 'med_4', 'rome']),
|
||||
...createSequentialRoutes(['med_4', 'med_5', 'med_6', 'barcelona']),
|
||||
...createSequentialRoutes(['med_6', 'med_7', 'gibraltar']),
|
||||
...createSequentialRoutes(['gibraltar', 'neur_1', 'neur_2', 'paris']),
|
||||
...createSequentialRoutes(['neur_2', 'neur_3']),
|
||||
...createSequentialRoutes(['istanbul', 'med_1']),
|
||||
|
||||
// Кипр - восточное Средиземное море
|
||||
...createSequentialRoutes(['limassol', 'med_e_1', 'med_e_2', 'athens']),
|
||||
...createSequentialRoutes(['limassol', 'med_e_1', 'med_1', 'istanbul']),
|
||||
|
||||
// Мадагаскар - Мозамбикский пролив
|
||||
...createSequentialRoutes(['eaf_5', 'mad_1', 'mad_2', 'mad_3', 'toamasina']),
|
||||
|
||||
// Черное море
|
||||
...createSequentialRoutes(['sevastopol', 'black_1', 'istanbul']),
|
||||
|
||||
// Индийский океан
|
||||
...createSequentialRoutes(['eaf_10', 'ind_1', 'ind_2', 'dubai']),
|
||||
...createSequentialRoutes(['ind_2', 'ind_3', 'ind_4', 'mumbai']),
|
||||
...createSequentialRoutes(['mumbai', 'ind_5', 'ind_6', 'ind_7', 'ind_8', 'ind_9']),
|
||||
|
||||
// Малаккский пролив
|
||||
...createSequentialRoutes(['ind_9', 'malacca_1', 'malacca_2', 'malacca_3', 'singapore']),
|
||||
...createSequentialRoutes(['malacca_2', 'bangkok']),
|
||||
// Белаван (Медан) - через Малаккский пролив
|
||||
...createSequentialRoutes(['belawan', 'malacca_1', 'malacca_2', 'singapore']),
|
||||
|
||||
// Яванское море - Джакарта
|
||||
...createSequentialRoutes(['jakarta', 'java_1', 'java_2', 'singapore']),
|
||||
|
||||
// Связь между портами Индонезии
|
||||
...createRoute('jakarta', 'belawan'),
|
||||
|
||||
// Южно-Китайское море
|
||||
...createSequentialRoutes(['singapore', 'scs_1', 'scs_2', 'scs_3', 'scs_4', 'scs_5', 'scs_6', 'scs_7', 'hongkong']),
|
||||
...createSequentialRoutes(['hongkong', 'scs_8', 'scs_9', 'scs_10', 'shanghai']),
|
||||
|
||||
// К Японии и Владивостоку
|
||||
...createSequentialRoutes(['shanghai', 'jap_1', 'jap_2', 'tokyo']),
|
||||
...createSequentialRoutes(['scs_10', 'jap_1', 'seoul']),
|
||||
...createSequentialRoutes(['tokyo', 'vladivostok']),
|
||||
...createSequentialRoutes(['vladivostok', 'seoul']),
|
||||
|
||||
// Тихий океан - западная часть
|
||||
...createSequentialRoutes(['tokyo', 'pac_w_1', 'pac_w_2', 'pac_w_3', 'pac_w_4', 'pac_w_5', 'pac_w_6', 'pac_w_7', 'pac_w_8']),
|
||||
|
||||
// К Австралии (с севера)
|
||||
...createSequentialRoutes(['pac_w_5', 'aus_1', 'aus_2', 'aus_3', 'aus_4', 'aus_5', 'aus_6', 'aus_7', 'sydney']),
|
||||
...createSequentialRoutes(['singapore', 'aus_1']),
|
||||
|
||||
// От Африки к Австралии через южный Индийский океан
|
||||
...createSequentialRoutes(['capetown', 'ind_s_1', 'ind_s_2', 'ind_s_3', 'ind_s_4', 'ind_s_5', 'ind_s_6', 'ind_s_7', 'ind_s_8', 'ind_s_9', 'ind_s_10', 'ind_s_11', 'ind_s_12', 'ind_s_13', 'aus_4']),
|
||||
...createSequentialRoutes(['nairobi', 'ind_s_1']),
|
||||
...createSequentialRoutes(['cape_3', 'ind_s_2']),
|
||||
|
||||
// К Новой Зеландии
|
||||
...createSequentialRoutes(['sydney', 'nz_1', 'nz_2', 'auckland']),
|
||||
|
||||
// Транс-Тихоокеанский маршрут
|
||||
...createSequentialRoutes(['tokyo', 'pac_c_1', 'pac_c_2', 'pac_c_3', 'pac_c_4', 'pac_c_5', 'pac_c_6', 'pac_c_7', 'pac_c_8', 'pac_e_1', 'losangeles']),
|
||||
|
||||
// ===== ПЕРЕКРЕСТНЫЕ СВЯЗИ (чтобы можно было переходить между маршрутами) =====
|
||||
|
||||
// Связи только между близкими параллельными маршрутами (не через весь океан)
|
||||
// Атлантика - Африка (только ближайшие точки)
|
||||
...createRoute('atl_mid_7', 'waf_1'),
|
||||
|
||||
// Карибское море - Центральная Атлантика (удалены прямые маршруты, должны проходить через промежуточные точки)
|
||||
|
||||
// Восточная Африка - Индийский океан (соединение двух путей)
|
||||
...createRoute('eaf_10', 'ind_1'),
|
||||
...createRoute('eaf_9', 'ind_1'),
|
||||
...createRoute('eaf_8', 'ind_2'),
|
||||
|
||||
// Средиземное море - Европа (больше связей)
|
||||
...createRoute('med_7', 'neur_1'),
|
||||
...createRoute('med_6', 'paris'),
|
||||
...createRoute('med_5', 'rome'),
|
||||
...createRoute('med_4', 'athens'),
|
||||
|
||||
// Гибралтар - связи
|
||||
...createRoute('gibraltar', 'barcelona'),
|
||||
...createRoute('gibraltar', 'tangier'),
|
||||
...createRoute('gibraltar', 'waf_1'),
|
||||
|
||||
// Северная Европа - Атлантика (только близкие)
|
||||
...createRoute('neur_3', 'atl_6'),
|
||||
...createRoute('neur_2', 'atl_6'),
|
||||
|
||||
// Панама - множественные подходы
|
||||
...createRoute('panama_canal', 'car_5'),
|
||||
...createRoute('panama_w', 'car_4'),
|
||||
|
||||
// Восточное побережье США - Атлантика (только ближайшие)
|
||||
...createRoute('us_east_2', 'car_1'),
|
||||
...createRoute('miami', 'car_1'),
|
||||
|
||||
// Тихий океан восток - Панама
|
||||
...createRoute('pac_e_9', 'panama_e'),
|
||||
...createRoute('pac_e_8', 'panama_canal'),
|
||||
|
||||
// Южная Америка - перекрёстные связи
|
||||
...createRoute('sa_pac_1', 'panama_e'),
|
||||
// Прямой маршрут sa_atl_10 -> car_6 удален (слишком короткий путь через океан)
|
||||
|
||||
// Индийский океан - Малакка (больше связей)
|
||||
...createRoute('ind_8', 'malacca_1'),
|
||||
...createRoute('ind_7', 'malacca_1'),
|
||||
|
||||
// Южно-Китайское море - Индийский океан
|
||||
...createRoute('scs_1', 'ind_9'),
|
||||
...createRoute('scs_1', 'malacca_3'),
|
||||
|
||||
// Австралия - множественные подходы
|
||||
...createRoute('aus_1', 'scs_1'),
|
||||
...createRoute('aus_1', 'ind_9'),
|
||||
|
||||
// Тихий океан запад - к разным направлениям
|
||||
...createRoute('pac_w_4', 'aus_1'),
|
||||
...createRoute('pac_w_3', 'aus_2'),
|
||||
|
||||
// Япония - Тихий океан (больше выходов)
|
||||
...createRoute('jap_2', 'pac_w_1'),
|
||||
...createRoute('tokyo', 'pac_w_1'),
|
||||
...createRoute('vladivostok', 'pac_w_1'),
|
||||
|
||||
// Санкт-Петербург связи
|
||||
...createRoute('stpetersburg', 'neur_5'),
|
||||
...createRoute('stpetersburg', 'london'),
|
||||
...createRoute('murmansk', 'north_3'),
|
||||
|
||||
// Транс-Тихоокеанский - связи с западом
|
||||
...createRoute('pac_c_1', 'pac_w_1'),
|
||||
...createRoute('pac_c_2', 'pac_w_2'),
|
||||
...createRoute('pac_c_3', 'pac_w_3'),
|
||||
|
||||
// Мыс Горн - связи
|
||||
...createRoute('sa_pac_12', 'horn_1'),
|
||||
...createRoute('horn_3', 'sa_atl_1'),
|
||||
|
||||
// Мыс Доброй Надежды - Индийский океан
|
||||
...createRoute('cape_3', 'ind_1'),
|
||||
|
||||
// Австралия - перекрестные связи
|
||||
...createRoute('aus_4', 'ind_s_13'),
|
||||
...createRoute('aus_3', 'ind_s_12'),
|
||||
...createRoute('melbourne', 'aus_7'),
|
||||
];
|
||||
|
||||
// Типы грузов для заданий (с весом и объемом)
|
||||
const cargoTypes = [
|
||||
{ id: 'electronics', name: 'Электроника', reward: 500, weight: 80, volume: 120 }, // Легкий, дорогой
|
||||
{ id: 'textiles', name: 'Текстиль', reward: 250, weight: 60, volume: 100 }, // Легкий, дешевый
|
||||
{ id: 'food', name: 'Продукты', reward: 300, weight: 100, volume: 150 }, // Легкий
|
||||
{ id: 'chemicals', name: 'Химикаты', reward: 400, weight: 150, volume: 180 }, // Средний
|
||||
{ id: 'furniture', name: 'Мебель', reward: 320, weight: 120, volume: 200 }, // Средний
|
||||
{ id: 'machinery', name: 'Оборудование', reward: 450, weight: 200, volume: 250 }, // Средне-тяжелый
|
||||
{ id: 'metals', name: 'Металлы', reward: 380, weight: 250, volume: 180 }, // Тяжелый
|
||||
{ id: 'automobiles', name: 'Автомобили', reward: 600, weight: 280, volume: 300 }, // Тяжелый, дорогой
|
||||
];
|
||||
|
||||
// Вместимость корабля (можно взять 2-3 средних груза или 1 тяжелый + 1-2 легких)
|
||||
const SHIP_MAX_WEIGHT = 600; // Максимальный вес
|
||||
const SHIP_MAX_VOLUME = 700; // Максимальный объем
|
||||
|
||||
// Цвета и иконки кораблей игроков
|
||||
const playerColors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12'];
|
||||
const playerShips = ['🚢', '⛴️', '🛳️', '⛵'];
|
||||
782
game.js
Normal file
782
game.js
Normal file
|
|
@ -0,0 +1,782 @@
|
|||
class Game {
|
||||
constructor() {
|
||||
this.players = [];
|
||||
this.currentPlayerIndex = 0;
|
||||
this.diceRolled = false;
|
||||
this.diceValue = 0;
|
||||
this.availableMissions = [];
|
||||
|
||||
this.setupStartModal();
|
||||
}
|
||||
|
||||
setupStartModal() {
|
||||
const playerCountBtns = document.querySelectorAll('.player-count-btn');
|
||||
let selectedCount = 1;
|
||||
|
||||
playerCountBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
playerCountBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
selectedCount = parseInt(btn.dataset.count);
|
||||
this.updatePlayerInputs(selectedCount);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('startGameBtn').addEventListener('click', () => {
|
||||
this.startGame(selectedCount);
|
||||
});
|
||||
}
|
||||
|
||||
updatePlayerInputs(count) {
|
||||
const container = document.getElementById('playerNamesContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'form-group';
|
||||
div.innerHTML = `
|
||||
<label>Имя игрока ${i + 1}:</label>
|
||||
<input type="text" id="player${i + 1}Name" value="Игрок ${i + 1}" class="player-name-input">
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
startGame(playerCount) {
|
||||
// Создаем игроков
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
const nameInput = document.getElementById(`player${i + 1}Name`);
|
||||
const name = nameInput ? nameInput.value : `Игрок ${i + 1}`;
|
||||
|
||||
this.players.push({
|
||||
name: name,
|
||||
location: 'newyork', // Все начинают в Нью-Йорке
|
||||
score: 0,
|
||||
missions: [], // Массив активных заданий
|
||||
completedMissionIds: [], // ID всех когда-либо взятых заданий (включая завершенные)
|
||||
currentWeight: 0,
|
||||
currentVolume: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Генерируем задания
|
||||
this.generateMissions();
|
||||
|
||||
// Закрываем стартовое окно
|
||||
document.getElementById('startModal').style.display = 'none';
|
||||
|
||||
// Инициализируем интерфейс
|
||||
this.initGame();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
initGame() {
|
||||
// Настройка кнопок
|
||||
document.getElementById('rollDiceBtn').addEventListener('click', () => this.rollDice());
|
||||
document.getElementById('endTurnBtn').addEventListener('click', () => this.endTurn());
|
||||
document.getElementById('newMissionBtn').addEventListener('click', () => this.showMissionsModal());
|
||||
|
||||
// Модальные окна
|
||||
document.querySelector('.close').addEventListener('click', () => {
|
||||
document.getElementById('missionModal').style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById('closeCompleteModal').addEventListener('click', () => {
|
||||
document.getElementById('completeModal').style.display = 'none';
|
||||
});
|
||||
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal')) {
|
||||
e.target.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateMissions() {
|
||||
this.availableMissions = [];
|
||||
|
||||
// Генерируем 15-20 случайных заданий
|
||||
const count = 15 + Math.floor(Math.random() * 6);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.availableMissions.push(this.generateSingleMission());
|
||||
}
|
||||
}
|
||||
|
||||
calculateRouteDistance(fromId, toId) {
|
||||
// BFS для нахождения кратчайшего пути
|
||||
const visited = new Set();
|
||||
const queue = [{id: fromId, distance: 0}];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const {id, distance} = queue.shift();
|
||||
|
||||
if (id === toId) {
|
||||
return distance;
|
||||
}
|
||||
|
||||
if (visited.has(id)) continue;
|
||||
visited.add(id);
|
||||
|
||||
const neighbors = routes
|
||||
.filter(r => r.from === id)
|
||||
.map(r => r.to);
|
||||
|
||||
neighbors.forEach(neighborId => {
|
||||
if (!visited.has(neighborId)) {
|
||||
queue.push({id: neighborId, distance: distance + 1});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return 20; // По умолчанию, если путь не найден
|
||||
}
|
||||
|
||||
generateSingleMission() {
|
||||
// Выбираем случайные локации (только города/порты, не путевые точки)
|
||||
const startLocations = locations.filter(l => l.type);
|
||||
const fromLocation = startLocations[Math.floor(Math.random() * startLocations.length)];
|
||||
let toLocation = startLocations[Math.floor(Math.random() * startLocations.length)];
|
||||
|
||||
// Убедимся, что это разные локации
|
||||
while (toLocation.id === fromLocation.id) {
|
||||
toLocation = startLocations[Math.floor(Math.random() * startLocations.length)];
|
||||
}
|
||||
|
||||
// Выбираем случайный груз
|
||||
const cargo = cargoTypes[Math.floor(Math.random() * cargoTypes.length)];
|
||||
|
||||
// Рассчитываем расстояние между городами
|
||||
const distance = this.calculateRouteDistance(fromLocation.id, toLocation.id);
|
||||
|
||||
// Награда зависит от расстояния и типа груза
|
||||
// Базовая награда груза + бонус за расстояние (50 очков за каждую точку маршрута)
|
||||
const distanceBonus = distance * 50;
|
||||
const totalReward = Math.floor(cargo.reward * 0.5 + distanceBonus);
|
||||
|
||||
return {
|
||||
id: 'mission_' + Date.now() + '_' + Math.random(),
|
||||
from: fromLocation.id,
|
||||
to: toLocation.id,
|
||||
fromName: fromLocation.name,
|
||||
toName: toLocation.name,
|
||||
cargo: cargo.name,
|
||||
cargoId: cargo.id,
|
||||
reward: totalReward,
|
||||
weight: cargo.weight,
|
||||
volume: cargo.volume,
|
||||
distance: distance
|
||||
};
|
||||
}
|
||||
|
||||
showMissionsModal() {
|
||||
const modal = document.getElementById('missionModal');
|
||||
const container = document.getElementById('missionsListModal');
|
||||
const player = this.getCurrentPlayer();
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
|
||||
<h4>Вместимость корабля:</h4>
|
||||
<p>Вес: ${player.currentWeight}/${SHIP_MAX_WEIGHT} кг</p>
|
||||
<p>Объем: ${player.currentVolume}/${SHIP_MAX_VOLUME} м³</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.availableMissions.forEach(mission => {
|
||||
const alreadyTaken = player.missions.find(m => m.id === mission.id);
|
||||
const wasTakenBefore = player.completedMissionIds.includes(mission.id);
|
||||
const canTake = !alreadyTaken &&
|
||||
!wasTakenBefore &&
|
||||
(player.currentWeight + mission.weight <= SHIP_MAX_WEIGHT) &&
|
||||
(player.currentVolume + mission.volume <= SHIP_MAX_VOLUME);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'mission-card' + (canTake ? '' : ' mission-card-disabled');
|
||||
card.innerHTML = `
|
||||
<div class="mission-card-header">
|
||||
${mission.fromName} ➜ ${mission.toName}
|
||||
</div>
|
||||
<div class="mission-card-details">
|
||||
<p>📦 Груз: ${mission.cargo}</p>
|
||||
<p>⚖️ Вес: ${mission.weight} кг | 📐 Объем: ${mission.volume} м³</p>
|
||||
<p>🗺️ Расстояние: ${mission.distance} шагов</p>
|
||||
${alreadyTaken ? '<p style="color: #f39c12;">✓ Уже взято!</p>' : ''}
|
||||
${wasTakenBefore ? '<p style="color: #e74c3c;">⚠️ Уже выполнялось ранее!</p>' : ''}
|
||||
${!canTake && !alreadyTaken && !wasTakenBefore ? '<p style="color: #e74c3c;">⚠️ Не помещается!</p>' : ''}
|
||||
</div>
|
||||
<div class="mission-card-reward">
|
||||
💰 Награда: ${mission.reward} очков
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (canTake) {
|
||||
card.addEventListener('click', () => {
|
||||
this.selectMission(mission);
|
||||
this.showMissionsModal(); // Обновляем модальное окно
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
selectMission(mission) {
|
||||
const player = this.getCurrentPlayer();
|
||||
|
||||
// Проверяем, не взято ли уже это задание
|
||||
if (player.missions.find(m => m.id === mission.id)) {
|
||||
alert('Вы уже взяли это задание!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, не брал ли игрок это задание ранее
|
||||
if (player.completedMissionIds.includes(mission.id)) {
|
||||
alert('Вы уже выполняли это задание ранее! Нельзя взять одно и то же задание дважды.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем вместимость корабля
|
||||
const totalWeight = player.currentWeight + mission.weight;
|
||||
const totalVolume = player.currentVolume + mission.volume;
|
||||
|
||||
if (totalWeight > SHIP_MAX_WEIGHT) {
|
||||
alert(`Превышен максимальный вес! (${totalWeight}/${SHIP_MAX_WEIGHT} кг)`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalVolume > SHIP_MAX_VOLUME) {
|
||||
alert(`Превышен максимальный объем! (${totalVolume}/${SHIP_MAX_VOLUME} м³)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Добавляем задание
|
||||
player.missions.push(mission);
|
||||
// Добавляем ID задания в список всех когда-либо взятых
|
||||
player.completedMissionIds.push(mission.id);
|
||||
player.currentWeight = totalWeight;
|
||||
player.currentVolume = totalVolume;
|
||||
|
||||
// НЕ удаляем задание из списка, чтобы другие игроки могли его взять
|
||||
// this.availableMissions = this.availableMissions.filter(m => m.id !== mission.id);
|
||||
|
||||
this.updateUI();
|
||||
this.highlightMissionDestinations();
|
||||
this.showNotification(`${player.name} взял груз: ${mission.cargo} (${mission.fromName} → ${mission.toName})`);
|
||||
}
|
||||
|
||||
rollDice() {
|
||||
if (this.gameEnded) {
|
||||
alert('Игра окончена! Обновите страницу для новой игры.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.diceRolled) {
|
||||
alert('Вы уже бросили кости! Выберите куда пойти или завершите ход.');
|
||||
return;
|
||||
}
|
||||
|
||||
const dice1El = document.getElementById('dice1');
|
||||
const dice2El = document.getElementById('dice2');
|
||||
|
||||
// Анимация
|
||||
dice1El.classList.add('rolling');
|
||||
dice2El.classList.add('rolling');
|
||||
|
||||
let counter = 0;
|
||||
const interval = setInterval(() => {
|
||||
dice1El.textContent = Math.floor(Math.random() * 6) + 1;
|
||||
dice2El.textContent = Math.floor(Math.random() * 6) + 1;
|
||||
counter++;
|
||||
|
||||
if (counter > 10) {
|
||||
clearInterval(interval);
|
||||
|
||||
// Финальные значения
|
||||
const dice1 = Math.floor(Math.random() * 6) + 1;
|
||||
const dice2 = Math.floor(Math.random() * 6) + 1;
|
||||
|
||||
dice1El.textContent = dice1;
|
||||
dice2El.textContent = dice2;
|
||||
dice1El.classList.remove('rolling');
|
||||
dice2El.classList.remove('rolling');
|
||||
|
||||
this.diceValue = dice1 + dice2;
|
||||
this.diceRolled = true;
|
||||
|
||||
document.getElementById('diceTotal').textContent = this.diceValue;
|
||||
document.getElementById('diceResult').style.display = 'block';
|
||||
document.getElementById('rollDiceBtn').disabled = true;
|
||||
|
||||
this.showAvailableMoves();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
showAvailableMoves() {
|
||||
const player = this.getCurrentPlayer();
|
||||
const currentLoc = player.location;
|
||||
|
||||
// Находим все доступные локации на расстоянии <= diceValue шагов
|
||||
const available = this.getReachableLocations(currentLoc, this.diceValue);
|
||||
|
||||
worldMap.setAvailableLocations(available);
|
||||
|
||||
this.showNotification(`${player.name} может пойти на ${this.diceValue} шагов. Выберите локацию на карте.`);
|
||||
}
|
||||
|
||||
getReachableLocations(startId, steps) {
|
||||
// BFS для поиска всех доступных локаций
|
||||
const visited = new Set();
|
||||
const queue = [{id: startId, steps: 0}];
|
||||
const reachable = [];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const {id, steps: currentSteps} = queue.shift();
|
||||
|
||||
if (visited.has(id)) continue;
|
||||
visited.add(id);
|
||||
|
||||
if (currentSteps > 0 && currentSteps <= steps) {
|
||||
reachable.push(id);
|
||||
}
|
||||
|
||||
if (currentSteps < steps) {
|
||||
// Находим соседей
|
||||
const neighbors = routes
|
||||
.filter(r => r.from === id || r.to === id)
|
||||
.map(r => r.from === id ? r.to : r.from);
|
||||
|
||||
neighbors.forEach(neighborId => {
|
||||
if (!visited.has(neighborId)) {
|
||||
queue.push({id: neighborId, steps: currentSteps + 1});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return reachable;
|
||||
}
|
||||
|
||||
onLocationClick(location) {
|
||||
if (!this.diceRolled) {
|
||||
// Показываем информацию о локации
|
||||
this.showLocationInfo(location);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, доступна ли эта локация
|
||||
if (!worldMap.availableLocations.includes(location.id)) {
|
||||
alert('Эта локация недоступна. Выберите подсвеченную локацию.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.movePlayer(location);
|
||||
}
|
||||
|
||||
async movePlayer(location) {
|
||||
const player = this.getCurrentPlayer();
|
||||
const oldLocation = player.location;
|
||||
|
||||
// Вычисляем путь
|
||||
const path = this.calculatePath(oldLocation, location.id);
|
||||
const stepsUsed = path.length - 1;
|
||||
|
||||
// Анимируем перемещение по пути
|
||||
await this.animatePlayerMovement(path);
|
||||
|
||||
// Устанавливаем финальную позицию
|
||||
player.location = location.id;
|
||||
this.diceValue -= stepsUsed;
|
||||
|
||||
if (this.diceValue <= 0) {
|
||||
// Все ходы использованы
|
||||
this.diceRolled = false;
|
||||
worldMap.clearAvailableLocations();
|
||||
document.getElementById('endTurnBtn').disabled = false;
|
||||
document.getElementById('diceResult').style.display = 'none';
|
||||
} else {
|
||||
// Еще остались ходы
|
||||
this.showAvailableMoves();
|
||||
this.showNotification(`${player.name} переместился в ${location.name}. Осталось ${this.diceValue} шагов.`);
|
||||
}
|
||||
|
||||
worldMap.draw();
|
||||
this.showLocationInfo(location);
|
||||
this.checkMissionCompletion();
|
||||
|
||||
if (this.diceValue <= 0) {
|
||||
this.showNotification(`${player.name} переместился в ${location.name}. Нажмите "Закончить ход".`);
|
||||
}
|
||||
}
|
||||
|
||||
async animatePlayerMovement(path) {
|
||||
const player = this.getCurrentPlayer();
|
||||
const playerIndex = this.currentPlayerIndex;
|
||||
|
||||
// Сохраняем оригинальную позицию
|
||||
const originalLocation = player.location;
|
||||
|
||||
// Для каждого шага пути
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
const currentLocationId = path[i - 1];
|
||||
const nextLocationId = path[i];
|
||||
|
||||
const currentLoc = allPoints.find(l => l.id === currentLocationId);
|
||||
const nextLoc = allPoints.find(l => l.id === nextLocationId);
|
||||
|
||||
if (!currentLoc || !nextLoc) {
|
||||
player.location = nextLocationId;
|
||||
worldMap.draw();
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Плавная анимация между точками
|
||||
const steps = 10; // Количество промежуточных кадров
|
||||
for (let step = 0; step <= steps; step++) {
|
||||
const progress = step / steps;
|
||||
|
||||
// Временно устанавливаем промежуточную позицию для визуализации
|
||||
const currentCoords = worldMap.getLocationCoords(currentLoc);
|
||||
const nextCoords = worldMap.getLocationCoords(nextLoc);
|
||||
|
||||
// Вычисляем промежуточную позицию
|
||||
const x = currentCoords.x + (nextCoords.x - currentCoords.x) * progress;
|
||||
const y = currentCoords.y + (nextCoords.y - currentCoords.y) * progress;
|
||||
|
||||
// Сохраняем временную позицию для отрисовки
|
||||
player._tempAnimationPos = { x, y };
|
||||
player.location = step === steps ? nextLocationId : currentLocationId;
|
||||
|
||||
// Перерисовываем карту
|
||||
worldMap.draw();
|
||||
|
||||
// Небольшая задержка для плавности
|
||||
await new Promise(resolve => setTimeout(resolve, 30));
|
||||
}
|
||||
|
||||
// Убираем временную позицию
|
||||
player.location = nextLocationId;
|
||||
delete player._tempAnimationPos;
|
||||
}
|
||||
|
||||
// Восстанавливаем финальную позицию
|
||||
player.location = path[path.length - 1];
|
||||
}
|
||||
|
||||
calculateDistance(fromId, toId) {
|
||||
const path = this.calculatePath(fromId, toId);
|
||||
return path ? path.length - 1 : 1;
|
||||
}
|
||||
|
||||
calculatePath(fromId, toId) {
|
||||
// BFS для нахождения кратчайшего пути с сохранением пути
|
||||
const visited = new Set();
|
||||
const queue = [{id: fromId, distance: 0, path: [fromId]}];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const {id, distance, path} = queue.shift();
|
||||
|
||||
if (id === toId) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (visited.has(id)) continue;
|
||||
visited.add(id);
|
||||
|
||||
const neighbors = routes
|
||||
.filter(r => r.from === id || r.to === id)
|
||||
.map(r => r.from === id ? r.to : r.from);
|
||||
|
||||
neighbors.forEach(neighborId => {
|
||||
if (!visited.has(neighborId)) {
|
||||
queue.push({id: neighborId, distance: distance + 1, path: [...path, neighborId]});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return [fromId, toId]; // По умолчанию прямой путь
|
||||
}
|
||||
|
||||
checkMissionCompletion() {
|
||||
const player = this.getCurrentPlayer();
|
||||
|
||||
// Проверяем все активные задания
|
||||
const completedMissions = player.missions.filter(m => m.to === player.location);
|
||||
|
||||
if (completedMissions.length > 0) {
|
||||
let totalReward = 0;
|
||||
let completedList = '';
|
||||
|
||||
completedMissions.forEach(mission => {
|
||||
player.score += mission.reward;
|
||||
totalReward += mission.reward;
|
||||
player.currentWeight -= mission.weight;
|
||||
player.currentVolume -= mission.volume;
|
||||
|
||||
completedList += `<p>📦 ${mission.cargo}: ${mission.fromName} → ${mission.toName} (+${mission.reward} очков)</p>`;
|
||||
|
||||
// Удаляем выполненное задание из активных
|
||||
player.missions = player.missions.filter(m => m.id !== mission.id);
|
||||
// ID уже сохранен в completedMissionIds, поэтому игрок не сможет взять его снова
|
||||
|
||||
// Генерируем новое задание
|
||||
this.availableMissions.push(this.generateSingleMission());
|
||||
});
|
||||
|
||||
const modal = document.getElementById('completeModal');
|
||||
const body = document.getElementById('completeModalBody');
|
||||
|
||||
body.innerHTML = `
|
||||
<p><strong>${player.name}</strong> доставил ${completedMissions.length} груз(ов)!</p>
|
||||
${completedList}
|
||||
<p style="font-size: 24px; color: #27ae60; font-weight: bold;">
|
||||
Всего: +${totalReward} очков
|
||||
</p>
|
||||
<p>Общий счет: <strong>${player.score}</strong></p>
|
||||
`;
|
||||
|
||||
modal.style.display = 'block';
|
||||
this.updateUI();
|
||||
|
||||
// Проверяем победу
|
||||
this.checkVictory();
|
||||
}
|
||||
}
|
||||
|
||||
checkVictory() {
|
||||
const WINNING_SCORE = 5000; // Победное количество очков
|
||||
|
||||
const winners = this.players.filter(p => p.score >= WINNING_SCORE);
|
||||
|
||||
if (winners.length > 0) {
|
||||
// Сортируем по очкам
|
||||
winners.sort((a, b) => b.score - a.score);
|
||||
const winner = winners[0];
|
||||
|
||||
setTimeout(() => {
|
||||
const modal = document.getElementById('completeModal');
|
||||
const body = document.getElementById('completeModalBody');
|
||||
|
||||
let winnersHTML = '';
|
||||
this.players.sort((a, b) => b.score - a.score).forEach((player, index) => {
|
||||
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '';
|
||||
winnersHTML += `<p>${medal} <strong>${player.name}</strong>: ${player.score} очков</p>`;
|
||||
});
|
||||
|
||||
body.innerHTML = `
|
||||
<h2 style="color: #f39c12; margin-bottom: 20px;">🎉 Игра окончена!</h2>
|
||||
<h3 style="color: #27ae60;">🏆 Победитель: ${winner.name}!</h3>
|
||||
<p style="font-size: 32px; font-weight: bold; color: #27ae60; margin: 20px 0;">
|
||||
${winner.score} очков
|
||||
</p>
|
||||
<hr style="margin: 20px 0;">
|
||||
<h4>Итоговые результаты:</h4>
|
||||
${winnersHTML}
|
||||
<hr style="margin: 20px 0;">
|
||||
<p style="margin-top: 20px;">Обновите страницу для новой игры</p>
|
||||
`;
|
||||
|
||||
modal.style.display = 'block';
|
||||
|
||||
// Блокируем дальнейшую игру
|
||||
this.gameEnded = true;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
showLocationInfo(location) {
|
||||
const infoBox = document.getElementById('locationInfo');
|
||||
|
||||
if (location.type) {
|
||||
// Это город/порт/хаб
|
||||
infoBox.innerHTML = `
|
||||
<h4>${location.icon} ${location.name}</h4>
|
||||
<p><strong>Страна:</strong> ${location.country}</p>
|
||||
<p><strong>Континент:</strong> ${location.continent}</p>
|
||||
<p><strong>Тип:</strong> ${
|
||||
location.type === 'port' ? '🚢 Порт' :
|
||||
location.type === 'city' ? '🏙️ Город' :
|
||||
'🏭 Логистический хаб'
|
||||
}</p>
|
||||
`;
|
||||
} else {
|
||||
// Это путевая точка
|
||||
infoBox.innerHTML = `
|
||||
<h4>🌊 ${location.name}</h4>
|
||||
<p><strong>Тип:</strong> Морской путь</p>
|
||||
<p class="mt-2">Промежуточная точка на торговом маршруте</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
endTurn() {
|
||||
this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length;
|
||||
this.diceRolled = false;
|
||||
this.diceValue = 0;
|
||||
|
||||
document.getElementById('diceResult').style.display = 'none';
|
||||
document.getElementById('rollDiceBtn').disabled = false;
|
||||
document.getElementById('endTurnBtn').disabled = true;
|
||||
document.getElementById('dice1').textContent = '?';
|
||||
document.getElementById('dice2').textContent = '?';
|
||||
|
||||
worldMap.clearAvailableLocations();
|
||||
|
||||
this.updateUI();
|
||||
this.showNotification(`Ход переходит к ${this.getCurrentPlayer().name}`);
|
||||
}
|
||||
|
||||
highlightMissionDestinations() {
|
||||
// Обновляем карту с подсветкой точек назначения
|
||||
worldMap.draw();
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
// Обновляем список игроков
|
||||
const playersList = document.getElementById('playersList');
|
||||
playersList.innerHTML = '';
|
||||
|
||||
this.players.forEach((player, index) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = `player-card player-${index + 1}`;
|
||||
if (index === this.currentPlayerIndex) {
|
||||
card.classList.add('active');
|
||||
}
|
||||
|
||||
const loc = allPoints.find(l => l.id === player.location);
|
||||
const WINNING_SCORE = 5000;
|
||||
const progress = Math.min(100, (player.score / WINNING_SCORE) * 100);
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="player-info">
|
||||
<span class="player-token">${playerShips[index]}</span>
|
||||
<div class="player-details">
|
||||
<div class="player-name-display">${player.name}</div>
|
||||
<div class="player-location">📍 ${loc ? loc.name : 'Неизвестно'}</div>
|
||||
<div style="margin-top: 5px;">
|
||||
<div style="background: #e0e0e0; height: 6px; border-radius: 3px; overflow: hidden;">
|
||||
<div style="background: ${playerColors[index]}; height: 100%; width: ${progress}%; transition: width 0.3s;"></div>
|
||||
</div>
|
||||
<div style="font-size: 10px; color: #999; margin-top: 2px;">
|
||||
${player.score} / ${WINNING_SCORE}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-score">${player.score}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
playersList.appendChild(card);
|
||||
});
|
||||
|
||||
// Обновляем текущего игрока в шапке
|
||||
document.getElementById('currentPlayerName').textContent = this.getCurrentPlayer().name;
|
||||
|
||||
// Обновляем активные задания
|
||||
const player = this.getCurrentPlayer();
|
||||
const missionBox = document.getElementById('currentMissionBox');
|
||||
|
||||
if (player.missions.length > 0) {
|
||||
let missionsHTML = '';
|
||||
player.missions.forEach((mission, index) => {
|
||||
const isAtDestination = player.location === mission.to;
|
||||
const isAtOrigin = player.location === mission.from;
|
||||
|
||||
missionsHTML += `
|
||||
<div class="active-mission ${isAtDestination ? 'mission-at-destination' : ''}">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>${index + 1}.</span>
|
||||
<span style="flex: 1; margin-left: 5px;">
|
||||
${isAtOrigin ? '📍' : isAtDestination ? '✅' : '🚢'}
|
||||
${mission.fromName} ➜ ${mission.toName}
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #666; margin-left: 15px;">
|
||||
📦 ${mission.cargo} | 💰 ${mission.reward}
|
||||
</div>
|
||||
${isAtDestination ? '<div style="color: #27ae60; font-size: 11px; margin-left: 15px;">⚡ Готов к разгрузке!</div>' : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
missionBox.innerHTML = `
|
||||
<div style="margin-bottom: 10px; font-size: 12px; color: #666;">
|
||||
Загрузка: ${player.currentWeight}/${SHIP_MAX_WEIGHT} кг | ${player.currentVolume}/${SHIP_MAX_VOLUME} м³
|
||||
</div>
|
||||
${missionsHTML}
|
||||
`;
|
||||
} else {
|
||||
missionBox.innerHTML = '<p class="no-mission">Корабль пуст</p>';
|
||||
}
|
||||
|
||||
worldMap.draw();
|
||||
}
|
||||
|
||||
getCurrentPlayer() {
|
||||
return this.players[this.currentPlayerIndex];
|
||||
}
|
||||
|
||||
showNotification(message) {
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
right: 30px;
|
||||
background: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
z-index: 10000;
|
||||
animation: slideInRight 0.5s, slideOutRight 0.5s 3s;
|
||||
max-width: 350px;
|
||||
`;
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Добавляем CSS анимацию если еще нет
|
||||
if (!document.getElementById('notificationStyles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'notificationStyles';
|
||||
style.textContent = `
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3500);
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация игры
|
||||
let worldMap, game;
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
worldMap = new WorldMap('worldMap');
|
||||
game = new Game();
|
||||
|
||||
console.log('🌍 Geo-Step Game загружена!');
|
||||
});
|
||||
156
index.html
Normal file
156
index.html
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Geo-Step - Настольная игра-путешествие по миру</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<!-- Верхняя панель -->
|
||||
<header class="game-header">
|
||||
<div class="header-left">
|
||||
<h1>🌍 Geo-Step Game</h1>
|
||||
<div class="game-subtitle">Настольная игра по изучению карты мира</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="current-player" id="currentPlayer">
|
||||
<span class="player-label">Ходит:</span>
|
||||
<span class="player-name" id="currentPlayerName">Игрок 1</span>
|
||||
</div>
|
||||
<div class="current-player" style="background: rgba(46, 204, 113, 0.2);">
|
||||
<span class="player-label">🏆 Цель:</span>
|
||||
<span class="player-name">5000</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Модальное окно начала игры -->
|
||||
<div id="startModal" class="modal" style="display: block;">
|
||||
<div class="modal-content">
|
||||
<h2>🎮 Начало игры</h2>
|
||||
<div class="start-game-form">
|
||||
<div class="form-group">
|
||||
<label>Количество игроков:</label>
|
||||
<div class="player-count-buttons">
|
||||
<button class="player-count-btn active" data-count="1">1</button>
|
||||
<button class="player-count-btn" data-count="2">2</button>
|
||||
<button class="player-count-btn" data-count="3">3</button>
|
||||
<button class="player-count-btn" data-count="4">4</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="playerNamesContainer">
|
||||
<div class="form-group">
|
||||
<label>Имя игрока 1:</label>
|
||||
<input type="text" id="player1Name" value="Игрок 1" class="player-name-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="startGameBtn" class="btn-primary btn-large">Начать игру</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Основная область игры -->
|
||||
<div class="game-main">
|
||||
<!-- Карта мира -->
|
||||
<div class="map-container">
|
||||
<canvas id="worldMap"></canvas>
|
||||
<div class="map-legend">
|
||||
<h4>Легенда</h4>
|
||||
<div class="legend-item">
|
||||
<span style="display: inline-block; width: 12px; height: 12px; background: #457b9d; border-radius: 50%; border: 2px solid white;"></span>
|
||||
Порт
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span style="display: inline-block; width: 12px; height: 12px; background: #e63946; border-radius: 50%; border: 2px solid white;"></span>
|
||||
Город
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span style="display: inline-block; width: 12px; height: 12px; background: #f77f00; border-radius: 50%; border: 2px solid white;"></span>
|
||||
Хаб
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span style="display: inline-block; width: 8px; height: 8px; background: white; border-radius: 50%; border: 1px solid #999;"></span>
|
||||
Морской путь
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span style="display: inline-block; width: 12px; height: 12px; background: #27ae60; border-radius: 50%; border: 2px solid white; box-shadow: 0 0 10px rgba(46,204,113,0.5);"></span>
|
||||
Пункт назначения
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Боковая панель с информацией -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>👥 Игроки</h2>
|
||||
</div>
|
||||
|
||||
<div class="players-list" id="playersList">
|
||||
<!-- Игроки будут добавлены динамически -->
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h3>🎲 Бросить кости</h3>
|
||||
<div class="dice-container">
|
||||
<div class="dice" id="dice1">?</div>
|
||||
<div class="dice" id="dice2">?</div>
|
||||
</div>
|
||||
<button id="rollDiceBtn" class="btn-primary btn-block">🎲 Бросить кости</button>
|
||||
<div class="dice-result" id="diceResult" style="display: none;">
|
||||
<p>Выпало: <span id="diceTotal" class="dice-total">0</span></p>
|
||||
<p class="dice-hint">Выберите куда пойти</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h3>📋 Текущее задание</h3>
|
||||
<div class="current-mission-box" id="currentMissionBox">
|
||||
<p class="no-mission">Нет активного задания</p>
|
||||
</div>
|
||||
<button id="newMissionBtn" class="btn-secondary btn-block">Взять новое задание</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h3>ℹ️ Информация о локации</h3>
|
||||
<div class="info-box" id="locationInfo">
|
||||
<p>Кликните на точку на карте</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<button id="endTurnBtn" class="btn-end-turn btn-block" disabled>Закончить ход</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно выбора задания -->
|
||||
<div id="missionModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h2>📦 Выберите задание</h2>
|
||||
<div id="missionsListModal" class="missions-grid">
|
||||
<!-- Задания будут добавлены динамически -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно завершения задания -->
|
||||
<div id="completeModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>🎉 Задание выполнено!</h2>
|
||||
<div id="completeModalBody"></div>
|
||||
<button id="closeCompleteModal" class="btn-primary">Продолжить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="data.js"></script>
|
||||
<script src="map.js"></script>
|
||||
<script src="game.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
320
map.js
Normal file
320
map.js
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
class WorldMap {
|
||||
constructor(canvasId) {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.hoveredLocation = null;
|
||||
this.availableLocations = [];
|
||||
this.mapImage = null;
|
||||
this.mapLoaded = false;
|
||||
|
||||
this.initCanvas();
|
||||
this.setupEventListeners();
|
||||
this.loadWorldMapImage();
|
||||
}
|
||||
|
||||
initCanvas() {
|
||||
this.canvas.width = this.canvas.offsetWidth;
|
||||
this.canvas.height = this.canvas.offsetHeight;
|
||||
}
|
||||
|
||||
loadWorldMapImage() {
|
||||
// Загружаем реальную карту мира
|
||||
this.mapImage = new Image();
|
||||
this.mapImage.crossOrigin = "anonymous";
|
||||
|
||||
// Используем бесплатную карту мира с Wikimedia Commons
|
||||
this.mapImage.src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Equirectangular_projection_SW.jpg/1280px-Equirectangular_projection_SW.jpg';
|
||||
|
||||
this.mapImage.onload = () => {
|
||||
this.mapLoaded = true;
|
||||
this.draw();
|
||||
};
|
||||
|
||||
this.mapImage.onerror = () => {
|
||||
console.warn('Не удалось загрузить карту, рисуем базовую версию');
|
||||
this.mapLoaded = false;
|
||||
this.draw();
|
||||
};
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||
this.canvas.addEventListener('click', (e) => this.onClick(e));
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.initCanvas();
|
||||
this.draw();
|
||||
});
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
this.hoveredLocation = this.getLocationAt(x, y);
|
||||
this.canvas.style.cursor = this.hoveredLocation ? 'pointer' : 'default';
|
||||
this.draw();
|
||||
}
|
||||
|
||||
onClick(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const location = this.getLocationAt(x, y);
|
||||
if (location) {
|
||||
game.onLocationClick(location);
|
||||
}
|
||||
}
|
||||
|
||||
getLocationAt(x, y) {
|
||||
for (const loc of allPoints) {
|
||||
const coords = this.getLocationCoords(loc);
|
||||
const dx = x - coords.x;
|
||||
const dy = y - coords.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance < 20) {
|
||||
return loc;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
drawWorldMap() {
|
||||
const ctx = this.ctx;
|
||||
|
||||
if (this.mapLoaded && this.mapImage) {
|
||||
// Рисуем загруженную карту мира
|
||||
ctx.drawImage(this.mapImage, 0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Добавляем легкое затемнение для лучшей видимости маркеров
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
} else {
|
||||
// Запасной вариант - простая карта
|
||||
ctx.fillStyle = '#a8dadc';
|
||||
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Рисуем простые континенты
|
||||
this.drawSimpleContinents();
|
||||
}
|
||||
}
|
||||
|
||||
drawSimpleContinents() {
|
||||
const ctx = this.ctx;
|
||||
ctx.fillStyle = '#90be6d';
|
||||
|
||||
// Упрощенные континенты
|
||||
// Европа
|
||||
ctx.fillRect(480, 170, 100, 120);
|
||||
|
||||
// Азия
|
||||
ctx.fillRect(650, 150, 350, 280);
|
||||
|
||||
// Африка
|
||||
ctx.fillRect(480, 280, 120, 260);
|
||||
|
||||
// Северная Америка
|
||||
ctx.fillRect(100, 180, 180, 180);
|
||||
|
||||
// Южная Америка
|
||||
ctx.fillRect(240, 360, 90, 200);
|
||||
|
||||
// Австралия
|
||||
ctx.fillRect(900, 480, 120, 100);
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.drawWorldMap();
|
||||
this.drawRoutes();
|
||||
this.drawLocations();
|
||||
this.drawPlayers();
|
||||
}
|
||||
|
||||
drawRoutes() {
|
||||
const ctx = this.ctx;
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.15)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.setLineDash([3, 3]);
|
||||
|
||||
routes.forEach(route => {
|
||||
const from = allPoints.find(l => l.id === route.from);
|
||||
const to = allPoints.find(l => l.id === route.to);
|
||||
|
||||
if (from && to) {
|
||||
const fromCoords = this.getLocationCoords(from);
|
||||
const toCoords = this.getLocationCoords(to);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(fromCoords.x, fromCoords.y);
|
||||
ctx.lineTo(toCoords.x, toCoords.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
getLocationCoords(loc) {
|
||||
// Пересчитываем координаты на основе текущего размера canvas
|
||||
const x = (loc.lon + 180) * (this.canvas.width / 360);
|
||||
const y = (90 - loc.lat) * (this.canvas.height / 180);
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
drawLocations() {
|
||||
const ctx = this.ctx;
|
||||
|
||||
// Получаем активные задания текущего игрока
|
||||
const activeMissionDestinations = [];
|
||||
if (game && game.getCurrentPlayer() && game.getCurrentPlayer().missions) {
|
||||
game.getCurrentPlayer().missions.forEach(mission => {
|
||||
activeMissionDestinations.push(mission.to);
|
||||
});
|
||||
}
|
||||
|
||||
allPoints.forEach(loc => {
|
||||
const coords = this.getLocationCoords(loc);
|
||||
const isHovered = this.hoveredLocation && this.hoveredLocation.id === loc.id;
|
||||
const isAvailable = this.availableLocations.includes(loc.id);
|
||||
const isWaypoint = !loc.type; // waypoints не имеют type
|
||||
const isMissionDestination = activeMissionDestinations.includes(loc.id);
|
||||
|
||||
// Подсветка точек назначения активных заданий
|
||||
if (isMissionDestination) {
|
||||
ctx.shadowColor = 'rgba(46, 204, 113, 0.8)';
|
||||
ctx.shadowBlur = 25;
|
||||
ctx.beginPath();
|
||||
ctx.arc(coords.x, coords.y, 30, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(46, 204, 113, 0.2)';
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Анимированный круг вокруг точки назначения
|
||||
ctx.strokeStyle = 'rgba(46, 204, 113, 0.6)';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Подсветка доступных локаций (желтая)
|
||||
if (isAvailable) {
|
||||
ctx.shadowColor = 'rgba(255, 215, 0, 0.8)';
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.beginPath();
|
||||
ctx.arc(coords.x, coords.y, 25, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(255, 215, 0, 0.3)';
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
// Размер точки
|
||||
const radius = isWaypoint ? (isHovered ? 8 : 5) : (isHovered ? 12 : 10);
|
||||
|
||||
// Круг локации
|
||||
ctx.beginPath();
|
||||
ctx.arc(coords.x, coords.y, radius, 0, Math.PI * 2);
|
||||
|
||||
if (isWaypoint) {
|
||||
// Путевые точки - маленькие белые точки
|
||||
ctx.fillStyle = isAvailable ? '#ffd700' : '#ffffff';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = isAvailable ? '#ff8c00' : 'rgba(0, 0, 0, 0.3)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
} else {
|
||||
// Города и порты - большие цветные
|
||||
if (isMissionDestination) {
|
||||
// Точки назначения - зеленые
|
||||
ctx.fillStyle = '#27ae60';
|
||||
} else if (loc.type === 'port') {
|
||||
ctx.fillStyle = '#457b9d';
|
||||
} else if (loc.type === 'city') {
|
||||
ctx.fillStyle = '#e63946';
|
||||
} else {
|
||||
ctx.fillStyle = '#f77f00';
|
||||
}
|
||||
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Название при наведении
|
||||
if (isHovered) {
|
||||
ctx.font = isWaypoint ? '12px Arial' : 'bold 14px Arial';
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.lineWidth = 3;
|
||||
|
||||
const textWidth = ctx.measureText(loc.name).width;
|
||||
const yOffset = isWaypoint ? -15 : -20;
|
||||
ctx.strokeText(loc.name, coords.x - textWidth / 2, coords.y + yOffset);
|
||||
ctx.fillText(loc.name, coords.x - textWidth / 2, coords.y + yOffset);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
drawPlayers() {
|
||||
if (!game || !game.players) return;
|
||||
|
||||
const ctx = this.ctx;
|
||||
|
||||
game.players.forEach((player, index) => {
|
||||
let coords;
|
||||
let offset = index * 25 - (game.players.length - 1) * 12;
|
||||
|
||||
// Если есть временная позиция анимации, используем её
|
||||
if (player._tempAnimationPos) {
|
||||
coords = player._tempAnimationPos;
|
||||
} else {
|
||||
const loc = allPoints.find(l => l.id === player.location);
|
||||
if (!loc) return;
|
||||
coords = this.getLocationCoords(loc);
|
||||
}
|
||||
|
||||
// Рисуем кораблик игрока
|
||||
ctx.font = 'bold 28px Arial';
|
||||
|
||||
// Тень для корабля
|
||||
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.fillText(playerShips[index], coords.x + offset - 14, coords.y + 38);
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Рамка вокруг текущего игрока
|
||||
if (game.currentPlayerIndex === index) {
|
||||
ctx.strokeStyle = playerColors[index];
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeRect(coords.x + offset - 18, coords.y + 15, 36, 36);
|
||||
}
|
||||
|
||||
// Индикатор активных заданий (не показываем во время анимации)
|
||||
if (!player._tempAnimationPos && player.missions && player.missions.length > 0) {
|
||||
ctx.fillStyle = '#f39c12';
|
||||
ctx.beginPath();
|
||||
ctx.arc(coords.x + offset + 12, coords.y + 20, 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Количество грузов
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = 'bold 10px Arial';
|
||||
ctx.fillText(player.missions.length, coords.x + offset + 9, coords.y + 24);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setAvailableLocations(locationIds) {
|
||||
this.availableLocations = locationIds;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
clearAvailableLocations() {
|
||||
this.availableLocations = [];
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
569
style.css
Normal file
569
style.css
Normal file
|
|
@ -0,0 +1,569 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Шапка игры */
|
||||
.game-header {
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
color: white;
|
||||
padding: 15px 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.game-subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.current-player {
|
||||
background: rgba(255,255,255,0.15);
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.player-label {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Основная область */
|
||||
.game-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Карта */
|
||||
.map-container {
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#worldMap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.map-legend h4 {
|
||||
margin-bottom: 10px;
|
||||
color: #1e3c72;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 5px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.legend-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Боковая панель */
|
||||
.sidebar {
|
||||
width: 380px;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.sidebar-section h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #1e3c72;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Список игроков */
|
||||
.players-list {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.player-card {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
border-left: 5px solid;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.player-card.active {
|
||||
background: #e3f2fd;
|
||||
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.player-card.player-1 { border-left-color: #e74c3c; }
|
||||
.player-card.player-2 { border-left-color: #3498db; }
|
||||
.player-card.player-3 { border-left-color: #2ecc71; }
|
||||
.player-card.player-4 { border-left-color: #f39c12; }
|
||||
|
||||
.player-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.player-token {
|
||||
font-size: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.player-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.player-name-display {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #1e3c72;
|
||||
}
|
||||
|
||||
.player-location {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.player-score {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
/* Кости */
|
||||
.dice-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dice {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: white;
|
||||
border: 3px solid #667eea;
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.dice.rolling {
|
||||
animation: diceRoll 0.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes diceRoll {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(-10deg); }
|
||||
75% { transform: rotate(10deg); }
|
||||
}
|
||||
|
||||
.dice-result {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: #e8f5e9;
|
||||
border-radius: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dice-total {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.dice-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Текущее задание */
|
||||
.current-mission-box {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.no-mission {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.mission-info {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mission-info p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.mission-route-text {
|
||||
font-weight: bold;
|
||||
color: #1e3c72;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.mission-reward-text {
|
||||
color: #27ae60;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Информационный блок */
|
||||
.info-box {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-box h4 {
|
||||
color: #1e3c72;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 5px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary, .btn-secondary, .btn-end-turn {
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
|
||||
.btn-end-turn {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-end-turn:hover:not(:disabled) {
|
||||
background: #229954;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 15px 30px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.6);
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
margin: 5% auto;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
width: 600px;
|
||||
max-width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||
animation: slideIn 0.3s;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Форма начала игры */
|
||||
.start-game-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
color: #1e3c72;
|
||||
}
|
||||
|
||||
.player-count-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.player-count-btn {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.player-count-btn:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.player-count-btn.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.player-name-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.player-name-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
/* Сетка заданий */
|
||||
.missions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mission-card {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.mission-card:hover {
|
||||
background: #e9ecef;
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.mission-card-header {
|
||||
font-weight: bold;
|
||||
color: #1e3c72;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mission-card-details {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.mission-card-reward {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
color: #27ae60;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mission-card-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.mission-card-disabled:hover {
|
||||
transform: none !important;
|
||||
background: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
.active-mission {
|
||||
background: #f8f9fa;
|
||||
padding: 8px;
|
||||
margin: 5px 0;
|
||||
border-radius: 5px;
|
||||
border-left: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.mission-at-destination {
|
||||
background: #e8f5e9;
|
||||
border-left-color: #27ae60;
|
||||
}
|
||||
|
||||
/* Скроллбар */
|
||||
.sidebar::-webkit-scrollbar,
|
||||
.modal-content::-webkit-scrollbar,
|
||||
.missions-grid::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-track,
|
||||
.modal-content::-webkit-scrollbar-track,
|
||||
.missions-grid::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb,
|
||||
.modal-content::-webkit-scrollbar-thumb,
|
||||
.missions-grid::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb:hover,
|
||||
.modal-content::-webkit-scrollbar-thumb:hover,
|
||||
.missions-grid::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Вспомогательные классы */
|
||||
.text-success { color: #27ae60; }
|
||||
.text-danger { color: #e74c3c; }
|
||||
.mt-2 { margin-top: 10px; }
|
||||
79
КАК_ЗАМЕНИТЬ_КАРТУ.md
Normal file
79
КАК_ЗАМЕНИТЬ_КАРТУ.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# 🗺️ Как заменить карту мира на свою
|
||||
|
||||
## Вариант 1: Использовать свой файл изображения
|
||||
|
||||
1. Положите файл с картой мира в папку проекта (например, `world-map.jpg` или `world-map.png`)
|
||||
|
||||
2. Откройте файл `map.js` и найдите строку 26:
|
||||
```javascript
|
||||
this.mapImage.src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Equirectangular_projection_SW.jpg/1280px-Equirectangular_projection_SW.jpg';
|
||||
```
|
||||
|
||||
3. Замените на:
|
||||
```javascript
|
||||
this.mapImage.src = 'world-map.jpg'; // или ваше имя файла
|
||||
```
|
||||
|
||||
## Вариант 2: Использовать другой URL
|
||||
|
||||
Вы можете использовать любое изображение карты из интернета.
|
||||
|
||||
Например:
|
||||
```javascript
|
||||
// Физическая карта
|
||||
this.mapImage.src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Equirectangular_projection_SW.jpg/1280px-Equirectangular_projection_SW.jpg';
|
||||
|
||||
// Политическая карта
|
||||
this.mapImage.src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Mercator_projection_SW.jpg/1280px-Mercator_projection_SW.jpg';
|
||||
|
||||
// Карта без подписей
|
||||
this.mapImage.src = 'https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73909/world.topo.bathy.200412.3x5400x2700.jpg';
|
||||
```
|
||||
|
||||
## Рекомендации по карте
|
||||
|
||||
### Идеальный формат:
|
||||
- **Разрешение:** 1280x640 или выше
|
||||
- **Формат:** JPG или PNG
|
||||
- **Проекция:** Равнопромежуточная (Equirectangular)
|
||||
- **Без текста:** Лучше карта без подписей стран
|
||||
|
||||
### Где найти карты:
|
||||
1. **Wikimedia Commons** - https://commons.wikimedia.org/wiki/Category:World_maps
|
||||
2. **Natural Earth** - https://www.naturalearthdata.com/
|
||||
3. **NASA Visible Earth** - https://visibleearth.nasa.gov/
|
||||
|
||||
## Настройка положения локаций
|
||||
|
||||
Если карта отличается от стандартной, возможно потребуется скорректировать координаты локаций в файле `data.js`.
|
||||
|
||||
Координаты задаются в пикселях:
|
||||
```javascript
|
||||
{ id: 'moscow', name: 'Москва', x: 640, y: 180, ... }
|
||||
```
|
||||
|
||||
Где:
|
||||
- `x` - положение по горизонтали (0 = левый край, 1100 = правый край)
|
||||
- `y` - положение по вертикали (0 = верхний край, 600 = нижний край)
|
||||
|
||||
## Отключить затемнение
|
||||
|
||||
Если карта слишком светлая или темная, можно изменить затемнение в `map.js` (строка 91):
|
||||
|
||||
```javascript
|
||||
// Убрать затемнение
|
||||
// ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
// ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Или изменить прозрачность (0.1 = слабое, 0.5 = сильное)
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
|
||||
```
|
||||
|
||||
## Размер canvas
|
||||
|
||||
Размер карты автоматически подстраивается под размер окна, но координаты локаций рассчитаны на:
|
||||
- Ширина: 1100px
|
||||
- Высота: 600px
|
||||
|
||||
Если хотите изменить размер, измените координаты локаций пропорционально.
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue