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 = `
Имя игрока ${i + 1}:
`;
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.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 загружена!');
});