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 загружена!'); });