geo-step-game/game.js
2025-11-02 16:42:25 +03:00

782 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class Game {
constructor() {
this.players = [];
this.currentPlayerIndex = 0;
this.diceRolled = false;
this.diceValue = 0;
this.availableMissions = [];
this.setupStartModal();
}
setupStartModal() {
const playerCountBtns = document.querySelectorAll('.player-count-btn');
let selectedCount = 1;
playerCountBtns.forEach(btn => {
btn.addEventListener('click', () => {
playerCountBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedCount = parseInt(btn.dataset.count);
this.updatePlayerInputs(selectedCount);
});
});
document.getElementById('startGameBtn').addEventListener('click', () => {
this.startGame(selectedCount);
});
}
updatePlayerInputs(count) {
const container = document.getElementById('playerNamesContainer');
container.innerHTML = '';
for (let i = 0; i < count; i++) {
const div = document.createElement('div');
div.className = 'form-group';
div.innerHTML = `
<label>Имя игрока ${i + 1}:</label>
<input type="text" id="player${i + 1}Name" value="Игрок ${i + 1}" class="player-name-input">
`;
container.appendChild(div);
}
}
startGame(playerCount) {
// Создаем игроков
for (let i = 0; i < playerCount; i++) {
const nameInput = document.getElementById(`player${i + 1}Name`);
const name = nameInput ? nameInput.value : `Игрок ${i + 1}`;
this.players.push({
name: name,
location: 'newyork', // Все начинают в Нью-Йорке
score: 0,
missions: [], // Массив активных заданий
completedMissionIds: [], // ID всех когда-либо взятых заданий (включая завершенные)
currentWeight: 0,
currentVolume: 0
});
}
// Генерируем задания
this.generateMissions();
// Закрываем стартовое окно
document.getElementById('startModal').style.display = 'none';
// Инициализируем интерфейс
this.initGame();
this.updateUI();
}
initGame() {
// Настройка кнопок
document.getElementById('rollDiceBtn').addEventListener('click', () => this.rollDice());
document.getElementById('endTurnBtn').addEventListener('click', () => this.endTurn());
document.getElementById('newMissionBtn').addEventListener('click', () => this.showMissionsModal());
// Модальные окна
document.querySelector('.close').addEventListener('click', () => {
document.getElementById('missionModal').style.display = 'none';
});
document.getElementById('closeCompleteModal').addEventListener('click', () => {
document.getElementById('completeModal').style.display = 'none';
});
window.addEventListener('click', (e) => {
if (e.target.classList.contains('modal')) {
e.target.style.display = 'none';
}
});
}
generateMissions() {
this.availableMissions = [];
// Генерируем 15-20 случайных заданий
const count = 15 + Math.floor(Math.random() * 6);
for (let i = 0; i < count; i++) {
this.availableMissions.push(this.generateSingleMission());
}
}
calculateRouteDistance(fromId, toId) {
// BFS для нахождения кратчайшего пути
const visited = new Set();
const queue = [{id: fromId, distance: 0}];
while (queue.length > 0) {
const {id, distance} = queue.shift();
if (id === toId) {
return distance;
}
if (visited.has(id)) continue;
visited.add(id);
const neighbors = routes
.filter(r => r.from === id)
.map(r => r.to);
neighbors.forEach(neighborId => {
if (!visited.has(neighborId)) {
queue.push({id: neighborId, distance: distance + 1});
}
});
}
return 20; // По умолчанию, если путь не найден
}
generateSingleMission() {
// Выбираем случайные локации (только города/порты, не путевые точки)
const startLocations = locations.filter(l => l.type);
const fromLocation = startLocations[Math.floor(Math.random() * startLocations.length)];
let toLocation = startLocations[Math.floor(Math.random() * startLocations.length)];
// Убедимся, что это разные локации
while (toLocation.id === fromLocation.id) {
toLocation = startLocations[Math.floor(Math.random() * startLocations.length)];
}
// Выбираем случайный груз
const cargo = cargoTypes[Math.floor(Math.random() * cargoTypes.length)];
// Рассчитываем расстояние между городами
const distance = this.calculateRouteDistance(fromLocation.id, toLocation.id);
// Награда зависит от расстояния и типа груза
// Базовая награда груза + бонус за расстояние (50 очков за каждую точку маршрута)
const distanceBonus = distance * 50;
const totalReward = Math.floor(cargo.reward * 0.5 + distanceBonus);
return {
id: 'mission_' + Date.now() + '_' + Math.random(),
from: fromLocation.id,
to: toLocation.id,
fromName: fromLocation.name,
toName: toLocation.name,
cargo: cargo.name,
cargoId: cargo.id,
reward: totalReward,
weight: cargo.weight,
volume: cargo.volume,
distance: distance
};
}
showMissionsModal() {
const modal = document.getElementById('missionModal');
const container = document.getElementById('missionsListModal');
const player = this.getCurrentPlayer();
container.innerHTML = `
<div style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
<h4>Вместимость корабля:</h4>
<p>Вес: ${player.currentWeight}/${SHIP_MAX_WEIGHT} кг</p>
<p>Объем: ${player.currentVolume}/${SHIP_MAX_VOLUME} м³</p>
</div>
`;
this.availableMissions.forEach(mission => {
const alreadyTaken = player.missions.find(m => m.id === mission.id);
const wasTakenBefore = player.completedMissionIds.includes(mission.id);
const canTake = !alreadyTaken &&
!wasTakenBefore &&
(player.currentWeight + mission.weight <= SHIP_MAX_WEIGHT) &&
(player.currentVolume + mission.volume <= SHIP_MAX_VOLUME);
const card = document.createElement('div');
card.className = 'mission-card' + (canTake ? '' : ' mission-card-disabled');
card.innerHTML = `
<div class="mission-card-header">
${mission.fromName}${mission.toName}
</div>
<div class="mission-card-details">
<p>📦 Груз: ${mission.cargo}</p>
<p>⚖️ Вес: ${mission.weight} кг | 📐 Объем: ${mission.volume} м³</p>
<p>🗺️ Расстояние: ${mission.distance} шагов</p>
${alreadyTaken ? '<p style="color: #f39c12;">✓ Уже взято!</p>' : ''}
${wasTakenBefore ? '<p style="color: #e74c3c;">⚠️ Уже выполнялось ранее!</p>' : ''}
${!canTake && !alreadyTaken && !wasTakenBefore ? '<p style="color: #e74c3c;">⚠️ Не помещается!</p>' : ''}
</div>
<div class="mission-card-reward">
💰 Награда: ${mission.reward} очков
</div>
`;
if (canTake) {
card.addEventListener('click', () => {
this.selectMission(mission);
this.showMissionsModal(); // Обновляем модальное окно
});
}
container.appendChild(card);
});
modal.style.display = 'block';
}
selectMission(mission) {
const player = this.getCurrentPlayer();
// Проверяем, не взято ли уже это задание
if (player.missions.find(m => m.id === mission.id)) {
alert('Вы уже взяли это задание!');
return;
}
// Проверяем, не брал ли игрок это задание ранее
if (player.completedMissionIds.includes(mission.id)) {
alert('Вы уже выполняли это задание ранее! Нельзя взять одно и то же задание дважды.');
return;
}
// Проверяем вместимость корабля
const totalWeight = player.currentWeight + mission.weight;
const totalVolume = player.currentVolume + mission.volume;
if (totalWeight > SHIP_MAX_WEIGHT) {
alert(`Превышен максимальный вес! (${totalWeight}/${SHIP_MAX_WEIGHT} кг)`);
return;
}
if (totalVolume > SHIP_MAX_VOLUME) {
alert(`Превышен максимальный объем! (${totalVolume}/${SHIP_MAX_VOLUME} м³)`);
return;
}
// Добавляем задание
player.missions.push(mission);
// Добавляем ID задания в список всех когда-либо взятых
player.completedMissionIds.push(mission.id);
player.currentWeight = totalWeight;
player.currentVolume = totalVolume;
// НЕ удаляем задание из списка, чтобы другие игроки могли его взять
// this.availableMissions = this.availableMissions.filter(m => m.id !== mission.id);
this.updateUI();
this.highlightMissionDestinations();
this.showNotification(`${player.name} взял груз: ${mission.cargo} (${mission.fromName}${mission.toName})`);
}
rollDice() {
if (this.gameEnded) {
alert('Игра окончена! Обновите страницу для новой игры.');
return;
}
if (this.diceRolled) {
alert('Вы уже бросили кости! Выберите куда пойти или завершите ход.');
return;
}
const dice1El = document.getElementById('dice1');
const dice2El = document.getElementById('dice2');
// Анимация
dice1El.classList.add('rolling');
dice2El.classList.add('rolling');
let counter = 0;
const interval = setInterval(() => {
dice1El.textContent = Math.floor(Math.random() * 6) + 1;
dice2El.textContent = Math.floor(Math.random() * 6) + 1;
counter++;
if (counter > 10) {
clearInterval(interval);
// Финальные значения
const dice1 = Math.floor(Math.random() * 6) + 1;
const dice2 = Math.floor(Math.random() * 6) + 1;
dice1El.textContent = dice1;
dice2El.textContent = dice2;
dice1El.classList.remove('rolling');
dice2El.classList.remove('rolling');
this.diceValue = dice1 + dice2;
this.diceRolled = true;
document.getElementById('diceTotal').textContent = this.diceValue;
document.getElementById('diceResult').style.display = 'block';
document.getElementById('rollDiceBtn').disabled = true;
this.showAvailableMoves();
}
}, 100);
}
showAvailableMoves() {
const player = this.getCurrentPlayer();
const currentLoc = player.location;
// Находим все доступные локации на расстоянии <= diceValue шагов
const available = this.getReachableLocations(currentLoc, this.diceValue);
worldMap.setAvailableLocations(available);
this.showNotification(`${player.name} может пойти на ${this.diceValue} шагов. Выберите локацию на карте.`);
}
getReachableLocations(startId, steps) {
// BFS для поиска всех доступных локаций
const visited = new Set();
const queue = [{id: startId, steps: 0}];
const reachable = [];
while (queue.length > 0) {
const {id, steps: currentSteps} = queue.shift();
if (visited.has(id)) continue;
visited.add(id);
if (currentSteps > 0 && currentSteps <= steps) {
reachable.push(id);
}
if (currentSteps < steps) {
// Находим соседей
const neighbors = routes
.filter(r => r.from === id || r.to === id)
.map(r => r.from === id ? r.to : r.from);
neighbors.forEach(neighborId => {
if (!visited.has(neighborId)) {
queue.push({id: neighborId, steps: currentSteps + 1});
}
});
}
}
return reachable;
}
onLocationClick(location) {
if (!this.diceRolled) {
// Показываем информацию о локации
this.showLocationInfo(location);
return;
}
// Проверяем, доступна ли эта локация
if (!worldMap.availableLocations.includes(location.id)) {
alert('Эта локация недоступна. Выберите подсвеченную локацию.');
return;
}
this.movePlayer(location);
}
async movePlayer(location) {
const player = this.getCurrentPlayer();
const oldLocation = player.location;
// Вычисляем путь
const path = this.calculatePath(oldLocation, location.id);
const stepsUsed = path.length - 1;
// Анимируем перемещение по пути
await this.animatePlayerMovement(path);
// Устанавливаем финальную позицию
player.location = location.id;
this.diceValue -= stepsUsed;
if (this.diceValue <= 0) {
// Все ходы использованы
this.diceRolled = false;
worldMap.clearAvailableLocations();
document.getElementById('endTurnBtn').disabled = false;
document.getElementById('diceResult').style.display = 'none';
} else {
// Еще остались ходы
this.showAvailableMoves();
this.showNotification(`${player.name} переместился в ${location.name}. Осталось ${this.diceValue} шагов.`);
}
worldMap.draw();
this.showLocationInfo(location);
this.checkMissionCompletion();
if (this.diceValue <= 0) {
this.showNotification(`${player.name} переместился в ${location.name}. Нажмите "Закончить ход".`);
}
}
async animatePlayerMovement(path) {
const player = this.getCurrentPlayer();
const playerIndex = this.currentPlayerIndex;
// Сохраняем оригинальную позицию
const originalLocation = player.location;
// Для каждого шага пути
for (let i = 1; i < path.length; i++) {
const currentLocationId = path[i - 1];
const nextLocationId = path[i];
const currentLoc = allPoints.find(l => l.id === currentLocationId);
const nextLoc = allPoints.find(l => l.id === nextLocationId);
if (!currentLoc || !nextLoc) {
player.location = nextLocationId;
worldMap.draw();
await new Promise(resolve => setTimeout(resolve, 200));
continue;
}
// Плавная анимация между точками
const steps = 10; // Количество промежуточных кадров
for (let step = 0; step <= steps; step++) {
const progress = step / steps;
// Временно устанавливаем промежуточную позицию для визуализации
const currentCoords = worldMap.getLocationCoords(currentLoc);
const nextCoords = worldMap.getLocationCoords(nextLoc);
// Вычисляем промежуточную позицию
const x = currentCoords.x + (nextCoords.x - currentCoords.x) * progress;
const y = currentCoords.y + (nextCoords.y - currentCoords.y) * progress;
// Сохраняем временную позицию для отрисовки
player._tempAnimationPos = { x, y };
player.location = step === steps ? nextLocationId : currentLocationId;
// Перерисовываем карту
worldMap.draw();
// Небольшая задержка для плавности
await new Promise(resolve => setTimeout(resolve, 30));
}
// Убираем временную позицию
player.location = nextLocationId;
delete player._tempAnimationPos;
}
// Восстанавливаем финальную позицию
player.location = path[path.length - 1];
}
calculateDistance(fromId, toId) {
const path = this.calculatePath(fromId, toId);
return path ? path.length - 1 : 1;
}
calculatePath(fromId, toId) {
// BFS для нахождения кратчайшего пути с сохранением пути
const visited = new Set();
const queue = [{id: fromId, distance: 0, path: [fromId]}];
while (queue.length > 0) {
const {id, distance, path} = queue.shift();
if (id === toId) {
return path;
}
if (visited.has(id)) continue;
visited.add(id);
const neighbors = routes
.filter(r => r.from === id || r.to === id)
.map(r => r.from === id ? r.to : r.from);
neighbors.forEach(neighborId => {
if (!visited.has(neighborId)) {
queue.push({id: neighborId, distance: distance + 1, path: [...path, neighborId]});
}
});
}
return [fromId, toId]; // По умолчанию прямой путь
}
checkMissionCompletion() {
const player = this.getCurrentPlayer();
// Проверяем все активные задания
const completedMissions = player.missions.filter(m => m.to === player.location);
if (completedMissions.length > 0) {
let totalReward = 0;
let completedList = '';
completedMissions.forEach(mission => {
player.score += mission.reward;
totalReward += mission.reward;
player.currentWeight -= mission.weight;
player.currentVolume -= mission.volume;
completedList += `<p>📦 ${mission.cargo}: ${mission.fromName}${mission.toName} (+${mission.reward} очков)</p>`;
// Удаляем выполненное задание из активных
player.missions = player.missions.filter(m => m.id !== mission.id);
// ID уже сохранен в completedMissionIds, поэтому игрок не сможет взять его снова
// Генерируем новое задание
this.availableMissions.push(this.generateSingleMission());
});
const modal = document.getElementById('completeModal');
const body = document.getElementById('completeModalBody');
body.innerHTML = `
<p><strong>${player.name}</strong> доставил ${completedMissions.length} груз(ов)!</p>
${completedList}
<p style="font-size: 24px; color: #27ae60; font-weight: bold;">
Всего: +${totalReward} очков
</p>
<p>Общий счет: <strong>${player.score}</strong></p>
`;
modal.style.display = 'block';
this.updateUI();
// Проверяем победу
this.checkVictory();
}
}
checkVictory() {
const WINNING_SCORE = 5000; // Победное количество очков
const winners = this.players.filter(p => p.score >= WINNING_SCORE);
if (winners.length > 0) {
// Сортируем по очкам
winners.sort((a, b) => b.score - a.score);
const winner = winners[0];
setTimeout(() => {
const modal = document.getElementById('completeModal');
const body = document.getElementById('completeModalBody');
let winnersHTML = '';
this.players.sort((a, b) => b.score - a.score).forEach((player, index) => {
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '';
winnersHTML += `<p>${medal} <strong>${player.name}</strong>: ${player.score} очков</p>`;
});
body.innerHTML = `
<h2 style="color: #f39c12; margin-bottom: 20px;">🎉 Игра окончена!</h2>
<h3 style="color: #27ae60;">🏆 Победитель: ${winner.name}!</h3>
<p style="font-size: 32px; font-weight: bold; color: #27ae60; margin: 20px 0;">
${winner.score} очков
</p>
<hr style="margin: 20px 0;">
<h4>Итоговые результаты:</h4>
${winnersHTML}
<hr style="margin: 20px 0;">
<p style="margin-top: 20px;">Обновите страницу для новой игры</p>
`;
modal.style.display = 'block';
// Блокируем дальнейшую игру
this.gameEnded = true;
}, 2000);
}
}
showLocationInfo(location) {
const infoBox = document.getElementById('locationInfo');
if (location.type) {
// Это город/порт/хаб
infoBox.innerHTML = `
<h4>${location.icon} ${location.name}</h4>
<p><strong>Страна:</strong> ${location.country}</p>
<p><strong>Континент:</strong> ${location.continent}</p>
<p><strong>Тип:</strong> ${
location.type === 'port' ? '🚢 Порт' :
location.type === 'city' ? '🏙️ Город' :
'🏭 Логистический хаб'
}</p>
`;
} else {
// Это путевая точка
infoBox.innerHTML = `
<h4>🌊 ${location.name}</h4>
<p><strong>Тип:</strong> Морской путь</p>
<p class="mt-2">Промежуточная точка на торговом маршруте</p>
`;
}
}
endTurn() {
this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length;
this.diceRolled = false;
this.diceValue = 0;
document.getElementById('diceResult').style.display = 'none';
document.getElementById('rollDiceBtn').disabled = false;
document.getElementById('endTurnBtn').disabled = true;
document.getElementById('dice1').textContent = '?';
document.getElementById('dice2').textContent = '?';
worldMap.clearAvailableLocations();
this.updateUI();
this.showNotification(`Ход переходит к ${this.getCurrentPlayer().name}`);
}
highlightMissionDestinations() {
// Обновляем карту с подсветкой точек назначения
worldMap.draw();
}
updateUI() {
// Обновляем список игроков
const playersList = document.getElementById('playersList');
playersList.innerHTML = '';
this.players.forEach((player, index) => {
const card = document.createElement('div');
card.className = `player-card player-${index + 1}`;
if (index === this.currentPlayerIndex) {
card.classList.add('active');
}
const loc = allPoints.find(l => l.id === player.location);
const WINNING_SCORE = 5000;
const progress = Math.min(100, (player.score / WINNING_SCORE) * 100);
card.innerHTML = `
<div class="player-info">
<span class="player-token">${playerShips[index]}</span>
<div class="player-details">
<div class="player-name-display">${player.name}</div>
<div class="player-location">📍 ${loc ? loc.name : 'Неизвестно'}</div>
<div style="margin-top: 5px;">
<div style="background: #e0e0e0; height: 6px; border-radius: 3px; overflow: hidden;">
<div style="background: ${playerColors[index]}; height: 100%; width: ${progress}%; transition: width 0.3s;"></div>
</div>
<div style="font-size: 10px; color: #999; margin-top: 2px;">
${player.score} / ${WINNING_SCORE}
</div>
</div>
</div>
<div class="player-score">${player.score}</div>
</div>
`;
playersList.appendChild(card);
});
// Обновляем текущего игрока в шапке
document.getElementById('currentPlayerName').textContent = this.getCurrentPlayer().name;
// Обновляем активные задания
const player = this.getCurrentPlayer();
const missionBox = document.getElementById('currentMissionBox');
if (player.missions.length > 0) {
let missionsHTML = '';
player.missions.forEach((mission, index) => {
const isAtDestination = player.location === mission.to;
const isAtOrigin = player.location === mission.from;
missionsHTML += `
<div class="active-mission ${isAtDestination ? 'mission-at-destination' : ''}">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>${index + 1}.</span>
<span style="flex: 1; margin-left: 5px;">
${isAtOrigin ? '📍' : isAtDestination ? '✅' : '🚢'}
${mission.fromName}${mission.toName}
</span>
</div>
<div style="font-size: 12px; color: #666; margin-left: 15px;">
📦 ${mission.cargo} | 💰 ${mission.reward}
</div>
${isAtDestination ? '<div style="color: #27ae60; font-size: 11px; margin-left: 15px;">⚡ Готов к разгрузке!</div>' : ''}
</div>
`;
});
missionBox.innerHTML = `
<div style="margin-bottom: 10px; font-size: 12px; color: #666;">
Загрузка: ${player.currentWeight}/${SHIP_MAX_WEIGHT} кг | ${player.currentVolume}/${SHIP_MAX_VOLUME} м³
</div>
${missionsHTML}
`;
} else {
missionBox.innerHTML = '<p class="no-mission">Корабль пуст</p>';
}
worldMap.draw();
}
getCurrentPlayer() {
return this.players[this.currentPlayerIndex];
}
showNotification(message) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 100px;
right: 30px;
background: white;
padding: 20px 30px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
z-index: 10000;
animation: slideInRight 0.5s, slideOutRight 0.5s 3s;
max-width: 350px;
`;
notification.textContent = message;
document.body.appendChild(notification);
// Добавляем CSS анимацию если еще нет
if (!document.getElementById('notificationStyles')) {
const style = document.createElement('style');
style.id = 'notificationStyles';
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
setTimeout(() => {
notification.remove();
}, 3500);
}
}
// Инициализация игры
let worldMap, game;
window.addEventListener('DOMContentLoaded', () => {
worldMap = new WorldMap('worldMap');
game = new Game();
console.log('🌍 Geo-Step Game загружена!');
});