init commit

This commit is contained in:
K. Krivoruchenko 2025-11-02 16:42:25 +03:00
commit f716b05e18
7 changed files with 2766 additions and 0 deletions

181
README.md Normal file
View 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
View 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
View 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
View 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">&times;</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
View 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
View 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; }

View 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
Если хотите изменить размер, измените координаты локаций пропорционально.