From f716b05e1868adc910b39d3d2822f03c223c8467 Mon Sep 17 00:00:00 2001 From: "K. Krivoruchenko" Date: Sun, 2 Nov 2025 16:42:25 +0300 Subject: [PATCH] init commit --- README.md | 181 ++++++++++ data.js | 679 ++++++++++++++++++++++++++++++++++++ game.js | 782 ++++++++++++++++++++++++++++++++++++++++++ index.html | 156 +++++++++ map.js | 320 +++++++++++++++++ style.css | 569 ++++++++++++++++++++++++++++++ КАК_ЗАМЕНИТЬ_КАРТУ.md | 79 +++++ 7 files changed, 2766 insertions(+) create mode 100644 README.md create mode 100644 data.js create mode 100644 game.js create mode 100644 index.html create mode 100644 map.js create mode 100644 style.css create mode 100644 КАК_ЗАМЕНИТЬ_КАРТУ.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f2dcd0 --- /dev/null +++ b/README.md @@ -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 + +## 🎯 Стратегии + +- **Планируйте маршруты** заранее, выбирая задания +- **Изучайте карту** — запоминайте расположение локаций +- **Выбирайте задания** с выгодными маршрутами +- **Используйте хабы** для быстрого перемещения между континентами + +## 🔮 Будущие обновления + +- [ ] Различные типы транспорта (корабль, поезд, самолет) +- [ ] Карточки событий (погода, задержки) +- [ ] Система уровней сложности +- [ ] Режим на время +- [ ] Режим соревнования +- [ ] Статистика и достижения +- [ ] Сохранение игры + +--- + +**Приятной игры и изучения географии! 🌍✈️🚢** diff --git a/data.js b/data.js new file mode 100644 index 0000000..1906f40 --- /dev/null +++ b/data.js @@ -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 = ['🚢', '⛴️', '🛳️', '⛵']; diff --git a/game.js b/game.js new file mode 100644 index 0000000..01873a7 --- /dev/null +++ b/game.js @@ -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 = ` + + + `; + 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 = ` +
+

Вместимость корабля:

+

Вес: ${player.currentWeight}/${SHIP_MAX_WEIGHT} кг

+

Объем: ${player.currentVolume}/${SHIP_MAX_VOLUME} м³

+
+ `; + + 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 = ` +
+ ${mission.fromName} ➜ ${mission.toName} +
+
+

📦 Груз: ${mission.cargo}

+

⚖️ Вес: ${mission.weight} кг | 📐 Объем: ${mission.volume} м³

+

🗺️ Расстояние: ${mission.distance} шагов

+ ${alreadyTaken ? '

✓ Уже взято!

' : ''} + ${wasTakenBefore ? '

⚠️ Уже выполнялось ранее!

' : ''} + ${!canTake && !alreadyTaken && !wasTakenBefore ? '

⚠️ Не помещается!

' : ''} +
+
+ 💰 Награда: ${mission.reward} очков +
+ `; + + 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 += `

📦 ${mission.cargo}: ${mission.fromName} → ${mission.toName} (+${mission.reward} очков)

`; + + // Удаляем выполненное задание из активных + 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 = ` +

${player.name} доставил ${completedMissions.length} груз(ов)!

+ ${completedList} +

+ Всего: +${totalReward} очков +

+

Общий счет: ${player.score}

+ `; + + 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 += `

${medal} ${player.name}: ${player.score} очков

`; + }); + + body.innerHTML = ` +

🎉 Игра окончена!

+

🏆 Победитель: ${winner.name}!

+

+ ${winner.score} очков +

+
+

Итоговые результаты:

+ ${winnersHTML} +
+

Обновите страницу для новой игры

+ `; + + modal.style.display = 'block'; + + // Блокируем дальнейшую игру + this.gameEnded = true; + }, 2000); + } + } + + showLocationInfo(location) { + const infoBox = document.getElementById('locationInfo'); + + if (location.type) { + // Это город/порт/хаб + infoBox.innerHTML = ` +

${location.icon} ${location.name}

+

Страна: ${location.country}

+

Континент: ${location.continent}

+

Тип: ${ + location.type === 'port' ? '🚢 Порт' : + location.type === 'city' ? '🏙️ Город' : + '🏭 Логистический хаб' + }

+ `; + } else { + // Это путевая точка + infoBox.innerHTML = ` +

🌊 ${location.name}

+

Тип: Морской путь

+

Промежуточная точка на торговом маршруте

+ `; + } + } + + 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 = ` +
+ ${playerShips[index]} +
+
${player.name}
+
📍 ${loc ? loc.name : 'Неизвестно'}
+
+
+
+
+
+ ${player.score} / ${WINNING_SCORE} +
+
+
+
${player.score}
+
+ `; + + 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 += ` +
+
+ ${index + 1}. + + ${isAtOrigin ? '📍' : isAtDestination ? '✅' : '🚢'} + ${mission.fromName} ➜ ${mission.toName} + +
+
+ 📦 ${mission.cargo} | 💰 ${mission.reward} +
+ ${isAtDestination ? '
⚡ Готов к разгрузке!
' : ''} +
+ `; + }); + + missionBox.innerHTML = ` +
+ Загрузка: ${player.currentWeight}/${SHIP_MAX_WEIGHT} кг | ${player.currentVolume}/${SHIP_MAX_VOLUME} м³ +
+ ${missionsHTML} + `; + } else { + missionBox.innerHTML = '

Корабль пуст

'; + } + + 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 загружена!'); +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..c594f1b --- /dev/null +++ b/index.html @@ -0,0 +1,156 @@ + + + + + + Geo-Step - Настольная игра-путешествие по миру + + + +
+ +
+
+

🌍 Geo-Step Game

+
Настольная игра по изучению карты мира
+
+
+
+ Ходит: + Игрок 1 +
+
+ 🏆 Цель: + 5000 +
+
+
+ + + + + +
+ +
+ +
+

Легенда

+
+ + Порт +
+
+ + Город +
+
+ + Хаб +
+
+ + Морской путь +
+
+ + Пункт назначения +
+
+
+ + + +
+ + + + + + +
+ + + + + + + diff --git a/map.js b/map.js new file mode 100644 index 0000000..187c66b --- /dev/null +++ b/map.js @@ -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(); + } +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..9a727b4 --- /dev/null +++ b/style.css @@ -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; } diff --git a/КАК_ЗАМЕНИТЬ_КАРТУ.md b/КАК_ЗАМЕНИТЬ_КАРТУ.md new file mode 100644 index 0000000..93ac532 --- /dev/null +++ b/КАК_ЗАМЕНИТЬ_КАРТУ.md @@ -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 + +Если хотите изменить размер, измените координаты локаций пропорционально. +