320 lines
12 KiB
JavaScript
320 lines
12 KiB
JavaScript
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();
|
||
}
|
||
}
|