turns-00030.parquet:57291
dbe53485fb4843ad4a5ca707
turn 1/1o1-mini-2024-09-12EnglishRussia7234 words
degenerate_repetitionAbsentFinal dense release
USER
исправь ошибки связаные с minimap
// game.js
// Использование ES6 модулей
class ResourceManager {
constructor(ui) {
this.resources = {
wood: 500,
iron: 300,
food: 200,
copper: 100,
gold: 500
};
this.ui = ui;
}
add(resource, amount) {
if (this.resources.hasOwnProperty(resource)) {
this.resources[resource] += amount;
this.ui.updateResource(resource, this.resources[resource]);
}
}
subtract(resource, amount) {
if (this.resources.hasOwnProperty(resource) && this.resources[resource] >= amount) {
this.resources[resource] -= amount;
this.ui.updateResource(resource, this.resources[resource]);
return true;
}
return false;
}
get(resource) {
return this.resources[resource] || 0;
}
getAll() {
return { ...this.resources };
}
setAll(newResources) {
this.resources = { ...newResources };
this.ui.updateAllResources(this.resources);
}
}
class Hex {
constructor(row, col, x, y, size) {
this.row = row;
this.col = col;
this.x = x;
this.y = y;
this.size = size;
this.owned = false;
this.captured = false;
this.isCastle = false;
this.hasFort = false;
this.resources = this.generateRandomResources();
this.troops = 0; // Количество войск на гексе
}
generateRandomResources() {
// Генерация рандомных значений для ресурсов
return {
wood: (Math.random() * 5 + 1).toFixed(1), // 1.0 - 6.0
iron: (Math.random() * 5 + 1).toFixed(1),
food: (Math.random() * 5 + 1).toFixed(1),
copper: (Math.random() * 5 + 1).toFixed(1),
gold: (Math.random() * 5 + 1).toFixed(1)
};
}
draw(ctx) {
const HEX_SIZE = this.size;
const angle = Math.PI / 3;
ctx.beginPath();
for (let i = 0; i < 6; i++) {
ctx.lineTo(
this.x + HEX_SIZE * Math.cos(angle * i),
this.y + HEX_SIZE * Math.sin(angle * i)
);
}
ctx.closePath();
if (this.isCastle) {
ctx.fillStyle = '#e74c3c'; // Красный для замка
} else if (this.hasFort) {
ctx.fillStyle = '#f1c40f'; // Желтый для форта
} else if (this.owned) {
ctx.fillStyle = '#2ecc71'; // Зеленый для захваченных гексов
} else {
ctx.fillStyle = '#ecf0f1'; // Белый для свободных гексов
}
ctx.fill();
ctx.strokeStyle = '#95a5a6';
ctx.stroke();
// Рисуем количество войск, если есть
if (this.troops > 0) {
ctx.fillStyle = '#fff';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(this.troops, this.x, this.y + 4);
}
}
isClicked(clickX, clickY) {
const dx = clickX - this.x;
const dy = clickY - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= this.size;
}
}
class Castle {
constructor(hex, ui) {
this.hex = hex;
this.hex.isCastle = true;
this.level = 1;
this.population = 1;
this.goldIncome = 10;
this.ui = ui;
this.barracks = 0; // Количество казарм
this.castleHealth = 500; // Начальное здоровье замка
}
upgrade(resourceManager) {
const upgradeCost = 100 * this.level;
if (resourceManager.subtract('gold', upgradeCost)) {
this.level += 1;
this.population += 2;
this.goldIncome += 10;
this.ui.updateCastle(this.level, this.population);
this.ui.logEvent(`Замок улучшен до уровня ${this.level}. Жители: ${this.population}, доход золота: ${this.goldIncome}`);
} else {
this.ui.displayAlert('Недостаточно золота для улучшения замка!');
}
}
buildBarracks(resourceManager) {
const costWood = 150;
const costIron = 100;
if (resourceManager.subtract('wood', costWood) && resourceManager.subtract('iron', costIron)) {
this.barracks += 1;
this.ui.logEvent(`Казарма построена. Всего казарм: ${this.barracks}`);
} else {
this.ui.displayAlert('Недостаточно ресурсов для строительства казармы!');
}
}
collectResources(resourceManager) {
resourceManager.add('gold', this.goldIncome);
this.ui.updateResource('gold', resourceManager.get('gold'));
}
}
class Building {
constructor(name, resource, increment, costResource, costAmount, ui) {
this.name = name;
this.resource = resource;
this.increment = increment;
this.costResource = costResource;
this.costAmount = costAmount;
this.level = 1;
this.ui = ui;
}
upgrade(resourceManager) {
const totalCost = this.costAmount * this.level;
if (resourceManager.subtract(this.costResource, totalCost)) {
this.level += 1;
this.increment += 5;
this.ui.logEvent(`${this.name} улучшена до уровня ${this.level}. Производство: ${this.increment} ${this.resource}/сек`);
} else {
this.ui.displayAlert(`Недостаточно ${this.costResource} для улучшения ${this.name}!`);
}
}
}
class UIManager {
constructor() {
this.resourcesElements = {
wood: document.getElementById('wood'),
iron: document.getElementById('iron'),
food: document.getElementById('food'),
copper: document.getElementById('copper'),
gold: document.getElementById('gold')
};
this.populationElement = document.getElementById('population');
this.castleLevelElement = document.getElementById('castleLevel');
this.logList = document.getElementById('logList');
this.tooltip = document.getElementById('tooltip');
this.contextMenu = document.getElementById('contextMenu');
this.contextMenuList = document.getElementById('contextMenuList');
this.troopModal = document.getElementById('troopModal');
this.closeModal = document.querySelector('.close');
this.buyTroopsButton = document.getElementById('buyTroops');
// Закрыть модальное окно при клике на крестик
this.closeModal.onclick = () => {
this.hideTroopModal();
};
// Закрыть модальное окно при клике вне его
window.onclick = (event) => {
if (event.target == this.troopModal) {
this.hideTroopModal();
}
};
}
updateResource(resource, value) {
if (this.resourcesElements[resource]) {
this.resourcesElements[resource].innerText = `${this.capitalize(resource)}: ${Math.floor(value)}`;
}
}
updateAllResources(resources) {
for (const [key, value] of Object.entries(resources)) {
this.updateResource(key, value);
}
}
updatePopulation(population) {
this.populationElement.innerText = `Жители: ${population}`;
}
updateCastle(level, population) {
this.castleLevelElement.innerText = `Уровень Замка: ${level}`;
this.updatePopulation(population);
}
logEvent(message) {
const li = document.createElement('li');
li.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
this.logList.prepend(li);
}
displayAlert(message) {
alert(message);
}
showContextMenu(options, x, y) {
// Очистить текущее содержание меню
this.contextMenuList.innerHTML = '';
// Добавить новые опции
options.forEach(option => {
const li = document.createElement('li');
li.id = option.id;
li.textContent = option.label;
this.contextMenuList.appendChild(li);
});
// Позиционировать и отображать меню
this.contextMenu.style.left = `${x}px`;
this.contextMenu.style.top = `${y}px`;
this.contextMenu.style.display = 'block';
}
hideContextMenu() {
this.contextMenu.style.display = 'none';
}
showTooltip(content, x, y) {
this.tooltip.innerHTML = content;
this.tooltip.style.left = `${x + 10}px`;
this.tooltip.style.top = `${y + 10}px`;
this.tooltip.style.display = 'block';
}
hideTooltip() {
this.tooltip.style.display = 'none';
}
showTroopModal() {
this.troopModal.style.display = 'block';
}
hideTroopModal() {
this.troopModal.style.display = 'none';
}
capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
}
class MiniMap {
constructor(mainCanvas, miniCanvas, grid) {
this.mainCanvas = mainCanvas;
this.miniCanvas = miniCanvas;
this.ctx = miniCanvas.getContext('2d');
this.grid = grid;
this.scale = this.miniCanvas.width / (50 * Math.sqrt(3) * 40); // Настройка пропорции для 50x50 карты
this.draw();
}
draw() {
this.ctx.clearRect(0, 0, this.miniCanvas.width, this.miniCanvas.height);
for (const hex of this.grid) {
if (hex.owned) {
this.ctx.fillStyle = hex.isCastle ? '#e74c3c' : (hex.hasFort ? '#f1c40f' : '#2ecc71');
// Пропустить гексы вне 50x50
if (hex.row >= 50 || hex.col >= 50) continue;
const x = hex.col * (this.miniCanvas.width / 50) + (hex.row % 2) * (this.miniCanvas.width / 100);
const y = hex.row * (this.miniCanvas.height / 50) * 0.75 + 10; // Добавлен отступ сверху
this.ctx.beginPath();
const size = (this.miniCanvas.width / 50) / 2;
const angle = Math.PI / 3;
for (let i = 0; i < 6; i++) {
this.ctx.lineTo(
x + size * Math.cos(angle * i),
y + size * Math.sin(angle * i)
);
}
this.ctx.closePath();
this.ctx.fill();
this.ctx.strokeStyle = '#ffffff';
this.ctx.stroke();
}
}
}
}
class ModalManager {
constructor(ui, game) {
this.ui = ui;
this.game = game;
this.buyTroopsButton = document.getElementById('buyTroops');
this.troopAmountInput = document.getElementById('troopAmount');
this.buyTroopsButton.addEventListener('click', () => {
this.buyTroops();
});
}
buyTroops() {
const amount = parseInt(this.troopAmountInput.value);
if (isNaN(amount) || amount < 1) {
this.ui.displayAlert('Пожалуйста, введите корректное количество войск.');
return;
}
const troopCost = 10 * amount; // Стоимость 10 еды за каждое войско
if (this.game.resourceManager.subtract('food', troopCost)) {
this.game.selectedHex.troops += amount;
this.ui.logEvent(`Размещено ${amount} войск на гексе (${this.game.selectedHex.row}, ${this.game.selectedHex.col}). Всего войск: ${this.game.selectedHex.troops}`);
this.ui.hideTroopModal();
} else {
this.ui.displayAlert('Недостаточно еды для покупки войск!');
}
}
}
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.ui = new UIManager();
this.resourceManager = new ResourceManager(this.ui);
this.hexSize = 40;
this.grid = [];
this.castle = null;
this.buildings = [];
this.selectedHex = null; // Для контекстного меню
this.tooltipHex = null; // Для подсказок
this.miniMapInstance = null;
this.panOffset = { x: 0, y: 0 };
this.viewport = { scale: 1 };
this.isPanning = false;
this.startPan = { x: 0, y: 0 };
this.modalManager = null;
this.setup();
}
setup() {
this.createGrid(50, 50); // 50x50 карта
this.placeStartingCastle();
this.initBuildings();
this.setupUIControls();
this.setupContextMenuHandlers();
this.mouseEvents();
this.initModalManager();
this.startResourceGeneration();
this.initMiniMap();
this.gameLoop();
}
createGrid(rows, cols) {
const HEX_SIZE = this.hexSize;
const HEX_WIDTH = Math.sqrt(3) * HEX_SIZE;
const HEX_HEIGHT = 2 * HEX_SIZE;
const HEX_HORZ_SPACING = HEX_WIDTH;
const HEX_VERT_SPACING = 1.5 * HEX_SIZE;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
let x = HEX_WIDTH * col + (row % 2) * (HEX_WIDTH / 2);
let y = HEX_HEIGHT * 0.75 * row + HEX_SIZE;
this.grid.push(new Hex(row, col, x, y, HEX_SIZE));
}
}
}
placeStartingCastle() {
const availableHexes = this.grid.filter(hex => !hex.owned);
const randomHex = availableHexes[Math.floor(Math.random() * availableHexes.length)];
randomHex.owned = true;
this.castle = new Castle(randomHex, this.ui);
this.resourceManager.add('gold', this.castle.goldIncome);
this.ui.logEvent(`Замок размещен на гексе (${randomHex.row}, ${randomHex.col}).`);
}
initBuildings() {
this.buildings.push(new Building('Лесопилка', 'wood', 5, 'wood', 50, this.ui));
this.buildings.push(new Building('Железная Шахта', 'iron', 5, 'iron', 50, this.ui));
this.buildings.push(new Building('Ферма', 'food', 5, 'food', 50, this.ui));
this.buildings.push(new Building('Медная Шахта', 'copper', 5, 'copper', 50, this.ui));
this.buildings.push(new Building('Золотая Шахта', 'gold', 5, 'gold', 50, this.ui));
}
setupUIControls() {
// Кнопки улучшения замка
document.getElementById('upgradeCastle').addEventListener('click', () => {
this.castle.upgrade(this.resourceManager);
});
// Кнопки строительства казарм
document.getElementById('buildBarracks').addEventListener('click', () => {
this.castle.buildBarracks(this.resourceManager);
});
// Кнопки сохранения и загрузки игры
document.getElementById('saveGame').addEventListener('click', () => {
this.saveGame();
});
document.getElementById('loadGame').addEventListener('click', () => {
this.loadGame();
});
// Кнопки улучшения зданий
document.getElementById('upgradeWood').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Лесопилка');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeIron').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Железная Шахта');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeFood').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Ферма');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeCopper').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Медная Шахта');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeGold').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Золотая Шахта');
building.upgrade(this.resourceManager);
});
}
setupContextMenuHandlers() {
// Обработка выбора пунктов контекстного меню
this.ui.contextMenuList.addEventListener('click', (e) => {
if (e.target && e.target.nodeName === 'LI') {
const action = e.target.id;
if (action === 'buildFort') {
if (this.selectedHex) {
this.buildFort(this.selectedHex);
this.ui.hideContextMenu();
}
} else if (action === 'placeTroops') {
if (this.selectedHex) {
this.openTroopModal(this.selectedHex);
this.ui.hideContextMenu();
}
} else if (action === 'upgradeCastleOption') {
this.castle.upgrade(this.resourceManager);
this.ui.hideContextMenu();
} else if (action === 'buildBarracksOption') {
this.castle.buildBarracks(this.resourceManager);
this.ui.hideContextMenu();
}
}
});
}
mouseEvents() {
// Обработка обычного клика для захвата гекса
this.canvas.addEventListener('click', (e) => {
const rect = this.canvas.getBoundingClientRect();
const clickX = (e.clientX - rect.left - this.panOffset.x) / this.viewport.scale;
const clickY = (e.clientY - rect.top - this.panOffset.y) / this.viewport.scale;
const clickedHex = this.getHexAt(clickX, clickY);
if (clickedHex && !clickedHex.owned && !clickedHex.isCastle) {
// Захват территории требует затрат ресурсов
const captureCost = {
wood: 50,
iron: 30,
gold: 20
};
const canCapture = Object.keys(captureCost).every(resource => this.resourceManager.get(resource) >= captureCost[resource]);
if (canCapture && this.isAdjacentToOwnedHex(clickedHex)) {
// Вычесть ресурсы
Object.keys(captureCost).forEach(resource => {
this.resourceManager.subtract(resource, captureCost[resource]);
});
this.captureHex(clickedHex);
} else if (!this.isAdjacentToOwnedHex(clickedHex)) {
this.ui.logEvent('Вы можете захватить только соседние гексы!');
} else {
this.ui.logEvent('Недостаточно ресурсов для захвата территории!');
}
}
});
// Обработка правого клика для контекстного меню
this.canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
const clickX = (e.clientX - rect.left - this.panOffset.x) / this.viewport.scale;
const clickY = (e.clientY - rect.top - this.panOffset.y) / this.viewport.scale;
const clickedHex = this.getHexAt(clickX, clickY);
if (clickedHex && clickedHex.owned) {
this.selectedHex = clickedHex; // Сохранить выбранный гекс
if (clickedHex.isCastle) {
// Если это замок, показать контекстное меню для замка
this.ui.showContextMenu([
{ id: 'upgradeCastleOption', label: 'Улучшить Замок' },
{ id: 'buildBarracksOption', label: 'Построить Казарму' }
], e.clientX, e.clientY);
} else {
// Иначе, показать обычное контекстное меню
this.ui.showContextMenu([
{ id: 'buildFort', label: 'Построить Форт' },
{ id: 'placeTroops', label: 'Разместить Войска' }
], e.clientX, e.clientY);
}
}
});
// Обработка наведения курсора для показа подсказок
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
const moveX = (e.clientX - rect.left - this.panOffset.x) / this.viewport.scale;
const moveY = (e.clientY - rect.top - this.panOffset.y) / this.viewport.scale;
const hoveredHex = this.getHexAt(moveX, moveY);
if (hoveredHex) {
const resources = hoveredHex.resources;
let content = `<strong>Ресурсы:</strong><br>`;
content += `Дерево: ${(resources.wood * 10).toFixed(1)} / сек<br>`;
content += `Железо: ${(resources.iron * 10).toFixed(1)} / сек<br>`;
content += `Еда: ${(resources.food * 10).toFixed(1)} / сек<br>`;
content += `Медь: ${(resources.copper * 10).toFixed(1)} / сек<br>`;
content += `Золото: ${(resources.gold * 10).toFixed(1)} / сек<br>`;
if (hoveredHex.hasFort) {
content += `<strong>Форт:</strong> Размещено войск: ${hoveredHex.troops}`;
}
this.ui.showTooltip(content, e.clientX, e.clientY);
this.tooltipHex = hoveredHex;
} else {
this.ui.hideTooltip();
this.tooltipHex = null;
}
});
// При уходе курсора с холста скрываем подсказку
this.canvas.addEventListener('mouseleave', () => {
this.ui.hideTooltip();
this.tooltipHex = null;
});
// Обработка перемещения по карте (панорамирование)
this.canvas.addEventListener('mousedown', (e) => {
if (e.button === 1 || (e.button === 0 && e.shiftKey)) { // Средняя кнопка или Shift + Левый
this.isPanning = true;
this.startPan.x = e.clientX;
this.startPan.y = e.clientY;
}
});
document.addEventListener('mouseup', (e) => {
this.isPanning = false;
});
document.addEventListener('mousemove', (e) => {
if (this.isPanning) {
const dx = e.clientX - this.startPan.x;
const dy = e.clientY - this.startPan.y;
this.panOffset.x += dx;
this.panOffset.y += dy;
this.startPan.x = e.clientX;
this.startPan.y = e.clientY;
}
});
// Обработка колесика мыши для масштабирования
this.canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const zoomIntensity = 0.1;
const delta = e.deltaY < 0 ? 1 : -1;
const factor = 1 + delta * zoomIntensity;
this.viewport.scale *= factor;
this.viewport.scale = Math.min(Math.max(this.viewport.scale, 0.5), 2);
});
}
initModalManager() {
this.modalManager = new ModalManager(this.ui, this);
}
openTroopModal(hex) {
if (hex.hasFort) {
this.ui.showTroopModal();
} else {
this.ui.displayAlert('На этом гексе не построен форт!');
}
}
getHexAt(x, y) {
return this.grid.find(hex => hex.isClicked(x, y));
}
isAdjacentToOwnedHex(targetHex) {
const neighbors = this.getNeighbors(targetHex);
return neighbors.some(hex => hex.owned);
}
getNeighbors(hex) {
const directions = [
{ dr: -1, dc: 0 }, { dr: -1, dc: 1 }, { dr: 0, dc: -1 },
{ dr: 0, dc: 1 }, { dr: 1, dc: -1 }, { dr: 1, dc: 0 }
];
const neighbors = [];
for (const dir of directions) {
const neighbor = this.grid.find(h => h.row === hex.row + dir.dr && h.col === hex.col + dir.dc);
if (neighbor) {
neighbors.push(neighbor);
}
}
return neighbors;
}
captureHex(hex) {
hex.owned = true;
this.ui.logEvent(`Гекс захвачен на (${hex.row}, ${hex.col}).`);
// Можно добавить дополнительные преимущества от захвата гекса
}
buildFort(hex) {
if (!hex.hasFort) {
const costWood = 50;
const costIron = 25;
if (this.resourceManager.subtract('wood', costWood) && this.resourceManager.subtract('iron', costIron)) {
hex.hasFort = true;
this.ui.logEvent(`Форт построен на гексе (${hex.row}, ${hex.col}).`);
} else {
this.ui.displayAlert('Недостаточно ресурсов для строительства форта!');
}
} else {
this.ui.displayAlert('На этом гексе уже построен форт!');
}
}
placeTroops(hex) {
if (hex.hasFort) {
if (this.castle.barracks > 0) {
this.openTroopModal(hex);
} else {
this.ui.displayAlert('Необходимо построить казармы в замке для размещения войск!');
}
} else {
this.ui.displayAlert('На этом гексе не построен форт!');
}
}
startResourceGeneration() {
// Генерация ресурсов каждые 1 секунду
setInterval(() => {
for (const building of this.buildings) {
this.resourceManager.add(building.resource, building.increment);
}
this.castle.collectResources(this.resourceManager);
}, 1000);
// Рост населения каждые 10 секунд в зависимости от уровня замка
setInterval(() => {
const newResidents = this.castle.level;
this.castle.population += newResidents;
this.ui.updatePopulation(this.castle.population);
this.ui.logEvent(`Население выросло на ${newResidents}. Всего жителей: ${this.castle.population}`);
}, 10000);
}
initMiniMap() {
this.miniMapInstance = new MiniMap(this.canvas, document.getElementById('miniMap'), this.grid);
}
gameLoop() {
this.ctx.save();
this.ctx.translate(this.panOffset.x, this.panOffset.y);
this.ctx.scale(this.viewport.scale, this.viewport.scale);
this.ctx.clearRect(-this.panOffset.x / this.viewport.scale, -this.panOffset.y / this.viewport.scale, this.canvas.width / this.viewport.scale, this.canvas.height / this.viewport.scale);
this.drawGrid();
this.ctx.restore();
this.drawMiniMap();
requestAnimationFrame(() => this.gameLoop());
}
drawGrid() {
for (const hex of this.grid) {
hex.draw(this.ctx);
}
}
drawMiniMap() {
// Обновление мини-карты
if (this.miniMapInstance) {
this.miniMapInstance.draw();
}
}
saveGame() {
const gameState = {
resources: this.resourceManager.getAll(),
castle: {
level: this.castle.level,
population: this.castle.population,
goldIncome: this.castle.goldIncome,
barracks: this.castle.barracks,
location: {
row: this.castle.hex.row,
col: this.castle.hex.col
},
castleHealth: this.castle.castleHealth
},
buildings: this.buildings.map(b => ({
name: b.name,
level: b.level,
increment: b.increment
})),
grid: this.grid.map(hex => ({
row: hex.row,
col: hex.col,
owned: hex.owned,
isCastle: hex.isCastle,
hasFort: hex.hasFort,
troops: hex.troops,
resources: hex.resources
}))
};
localStorage.setItem('hexGameSave', JSON.stringify(gameState));
this.ui.logEvent('Игра сохранена.');
}
loadGame() {
const savedState = localStorage.getItem('hexGameSave');
if (savedState) {
const parsedState = JSON.parse(savedState);
this.resourceManager.setAll(parsedState.resources);
// Восстановление замка
const castleLocation = parsedState.castle.location;
const castleHex = this.grid.find(hex => hex.row === castleLocation.row && hex.col === castleLocation.col);
if (castleHex) {
this.castle = new Castle(castleHex, this.ui);
this.castle.level = parsedState.castle.level;
this.castle.population = parsedState.castle.population;
this.castle.goldIncome = parsedState.castle.goldIncome;
this.castle.barracks = parsedState.castle.barracks;
this.castle.castleHealth = parsedState.castle.castleHealth;
this.ui.updateCastle(this.castle.level, this.castle.population);
}
// Восстановление зданий
for (let i = 0; i < this.buildings.length; i++) {
const savedBuilding = parsedState.buildings.find(b => b.name === this.buildings[i].name);
if (savedBuilding) {
this.buildings[i].level = savedBuilding.level;
this.buildings[i].increment = savedBuilding.increment;
}
}
// Восстановление гексов
for (let i = 0; i < this.grid.length; i++) {
const savedHex = parsedState.grid[i];
this.grid[i].owned = savedHex.owned;
this.grid[i].isCastle = savedHex.isCastle;
this.grid[i].hasFort = savedHex.hasFort;
this.grid[i].troops = savedHex.troops;
this.grid[i].resources = savedHex.resources;
if (this.grid[i].isCastle) {
this.castle.hex = this.grid[i];
}
}
this.ui.updateAllResources(this.resourceManager.getAll());
this.ui.logEvent('Игра загружена.');
} else {
this.ui.displayAlert('Сохранения не найдены!');
}
}
}
class MiniMap {
constructor(mainCanvas, miniCanvas, grid) {
this.mainCanvas = mainCanvas;
this.miniCanvas = miniCanvas;
this.ctx = miniCanvas.getContext('2d');
this.grid = grid;
// Расчет масштаба для 50x50 карты, предполагая размер каждого гекса 40px
this.scale = this.miniCanvas.width / (50 * Math.sqrt(3) * 40); // Пропорция для 50x50 карты
this.draw();
}
draw() {
this.ctx.clearRect(0, 0, this.miniCanvas.width, this.miniCanvas.height);
for (const hex of this.grid) {
if (hex.owned) {
this.ctx.fillStyle = hex.isCastle ? '#e74c3c' : (hex.hasFort ? '#f1c40f' : '#2ecc71');
// Пропустить гексы вне 50x50
if (hex.row >= 50 || hex.col >= 50) continue;
const x = hex.col * (this.miniCanvas.width / 50) + (hex.row % 2) * (this.miniCanvas.width / 100);
const y = hex.row * (this.miniCanvas.height / 50) * 0.75 + 10; // Добавлен отступ сверху
this.ctx.beginPath();
const size = (this.miniCanvas.width / 50) / 2;
const angle = Math.PI / 3;
for (let i = 0; i < 6; i++) {
this.ctx.lineTo(
x + size * Math.cos(angle * i),
y + size * Math.sin(angle * i)
);
}
this.ctx.closePath();
this.ctx.fill();
this.ctx.strokeStyle = '#ffffff';
this.ctx.stroke();
}
}
}
}
class ModalManager {
constructor(ui, game) {
this.ui = ui;
this.game = game;
this.buyTroopsButton = document.getElementById('buyTroops');
this.troopAmountInput = document.getElementById('troopAmount');
this.buyTroopsButton.addEventListener('click', () => {
this.buyTroops();
});
}
buyTroops() {
const amount = parseInt(this.troopAmountInput.value);
if (isNaN(amount) || amount < 1) {
this.ui.displayAlert('Пожалуйста, введите корректное количество войск.');
return;
}
const troopCost = 10 * amount; // Стоимость 10 еды за каждое войско
if (this.game.resourceManager.subtract('food', troopCost)) {
this.game.selectedHex.troops += amount;
this.ui.logEvent(`Размещено ${amount} войск на гексе (${this.game.selectedHex.row}, ${this.game.selectedHex.col}). Всего войск: ${this.game.selectedHex.troops}`);
this.ui.hideTroopModal();
} else {
this.ui.displayAlert('Недостаточно еды для покупки войск!');
}
}
}
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.ui = new UIManager();
this.resourceManager = new ResourceManager(this.ui);
this.hexSize = 40;
this.grid = [];
this.castle = null;
this.buildings = [];
this.selectedHex = null; // Для контекстного меню
this.tooltipHex = null; // Для подсказок
this.miniMapInstance = null;
this.panOffset = { x: 0, y: 0 };
this.viewport = { scale: 1 };
this.isPanning = false;
this.startPan = { x: 0, y: 0 };
this.modalManager = null;
this.setup();
}
setup() {
this.createGrid(50, 50); // 50x50 карта
this.placeStartingCastle();
this.initBuildings();
this.setupUIControls();
this.setupContextMenuHandlers();
this.mouseEvents();
this.initModalManager();
this.startResourceGeneration();
this.initMiniMap();
this.gameLoop();
}
createGrid(rows, cols) {
const HEX_SIZE = this.hexSize;
const HEX_WIDTH = Math.sqrt(3) * HEX_SIZE;
const HEX_HEIGHT = 2 * HEX_SIZE;
const HEX_HORZ_SPACING = HEX_WIDTH;
const HEX_VERT_SPACING = 1.5 * HEX_SIZE;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
let x = HEX_WIDTH * col + (row % 2) * (HEX_WIDTH / 2);
let y = HEX_HEIGHT * 0.75 * row + HEX_SIZE;
this.grid.push(new Hex(row, col, x, y, HEX_SIZE));
}
}
}
placeStartingCastle() {
const availableHexes = this.grid.filter(hex => !hex.owned);
const randomHex = availableHexes[Math.floor(Math.random() * availableHexes.length)];
randomHex.owned = true;
this.castle = new Castle(randomHex, this.ui);
this.resourceManager.add('gold', this.castle.goldIncome);
this.ui.logEvent(`Замок размещен на гексе (${randomHex.row}, ${randomHex.col}).`);
}
initBuildings() {
this.buildings.push(new Building('Лесопилка', 'wood', 5, 'wood', 50, this.ui));
this.buildings.push(new Building('Железная Шахта', 'iron', 5, 'iron', 50, this.ui));
this.buildings.push(new Building('Ферма', 'food', 5, 'food', 50, this.ui));
this.buildings.push(new Building('Медная Шахта', 'copper', 5, 'copper', 50, this.ui));
this.buildings.push(new Building('Золотая Шахта', 'gold', 5, 'gold', 50, this.ui));
}
setupUIControls() {
// Кнопки улучшения замка
document.getElementById('upgradeCastle').addEventListener('click', () => {
this.castle.upgrade(this.resourceManager);
});
// Кнопки строительства казарм
document.getElementById('buildBarracks').addEventListener('click', () => {
this.castle.buildBarracks(this.resourceManager);
});
// Кнопки сохранения и загрузки игры
document.getElementById('saveGame').addEventListener('click', () => {
this.saveGame();
});
document.getElementById('loadGame').addEventListener('click', () => {
this.loadGame();
});
// Кнопки улучшения зданий
document.getElementById('upgradeWood').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Лесопилка');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeIron').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Железная Шахта');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeFood').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Ферма');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeCopper').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Медная Шахта');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeGold').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Золотая Шахта');
building.upgrade(this.resourceManager);
});
}
setupContextMenuHandlers() {
// Обработка выбора пунктов контекстного меню
this.ui.contextMenuList.addEventListener('click', (e) => {
if (e.target && e.target.nodeName === 'LI') {
const action = e.target.id;
if (action === 'buildFort') {
if (this.selectedHex) {
this.buildFort(this.selectedHex);
this.ui.hideContextMenu();
}
} else if (action === 'placeTroops') {
if (this.selectedHex) {
this.openTroopModal(this.selectedHex);
this.ui.hideContextMenu();
}
} else if (action === 'upgradeCastleOption') {
this.castle.upgrade(this.resourceManager);
this.ui.hideContextMenu();
} else if (action === 'buildBarracksOption') {
this.castle.buildBarracks(this.resourceManager);
this.ui.hideContextMenu();
}
}
});
}
mouseEvents() {
// Обработка обычного клика для захвата гекса
this.canvas.addEventListener('click', (e) => {
const rect = this.canvas.getBoundingClientRect();
const clickX = (e.clientX - rect.left - this.panOffset.x) / this.viewport.scale;
const clickY = (e.clientY - rect.top - this.panOffset.y) / this.viewport.scale;
const clickedHex = this.getHexAt(clickX, clickY);
if (clickedHex && !clickedHex.owned && !clickedHex.isCastle) {
// Захват территории требует затрат ресурсов
const captureCost = {
wood: 50,
iron: 30,
gold: 20
};
const canCapture = Object.keys(captureCost).every(resource => this.resourceManager.get(resource) >= captureCost[resource]);
if (canCapture && this.isAdjacentToOwnedHex(clickedHex)) {
// Вычесть ресурсы
Object.keys(captureCost).forEach(resource => {
this.resourceManager.subtract(resource, captureCost[resource]);
});
this.captureHex(clickedHex);
} else if (!this.isAdjacentToOwnedHex(clickedHex)) {
this.ui.logEvent('Вы можете захватить только соседние гексы!');
} else {
this.ui.logEvent('Недостаточно ресурсов для захвата территории!');
}
}
});
// Обработка правого клика для контекстного меню
this.canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
const clickX = (e.clientX - rect.left - this.panOffset.x) / this.viewport.scale;
const clickY = (e.clientY - rect.top - this.panOffset.y) / this.viewport.scale;
const clickedHex = this.getHexAt(clickX, clickY);
if (clickedHex && clickedHex.owned) {
this.selectedHex = clickedHex; // Сохранить выбранный гекс
if (clickedHex.isCastle) {
// Если это замок, показать контекстное меню для замка
this.ui.showContextMenu([
{ id: 'upgradeCastleOption', label: 'Улучшить Замок' },
{ id: 'buildBarracksOption', label: 'Построить Казарму' }
], e.clientX, e.clientY);
} else {
// Иначе, показать обычное контекстное меню
this.ui.showContextMenu([
{ id: 'buildFort', label: 'Построить Форт' },
{ id: 'placeTroops', label: 'Разместить Войска' }
], e.clientX, e.clientY);
}
}
});
// Обработка наведения курсора для показа подсказок
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
const moveX = (e.clientX - rect.left - this.panOffset.x) / this.viewport.scale;
const moveY = (e.clientY - rect.top - this.panOffset.y) / this.viewport.scale;
const hoveredHex = this.getHexAt(moveX, moveY);
if (hoveredHex) {
const resources = hoveredHex.resources;
let content = `<strong>Ресурсы:</strong><br>`;
content += `Дерево: ${(resources.wood * 10).toFixed(1)} / сек<br>`;
content += `Железо: ${(resources.iron * 10).toFixed(1)} / сек<br>`;
content += `Еда: ${(resources.food * 10).toFixed(1)} / сек<br>`;
content += `Медь: ${(resources.copper * 10).toFixed(1)} / сек<br>`;
content += `Золото: ${(resources.gold * 10).toFixed(1)} / сек<br>`;
if (hoveredHex.hasFort) {
content += `<strong>Форт:</strong> Размещено войск: ${hoveredHex.troops}`;
}
this.ui.showTooltip(content, e.clientX, e.clientY);
this.tooltipHex = hoveredHex;
} else {
this.ui.hideTooltip();
this.tooltipHex = null;
}
});
// При уходе курсора с холста скрываем подсказку
this.canvas.addEventListener('mouseleave', () => {
this.ui.hideTooltip();
this.tooltipHex = null;
});
// Обработка перемещения по карте (панорамирование)
this.canvas.addEventListener('mousedown', (e) => {
if (e.button === 1 || (e.button === 0 && e.shiftKey)) { // Средняя кнопка или Shift + Левый
this.isPanning = true;
this.startPan.x = e.clientX;
this.startPan.y = e.clientY;
}
});
document.addEventListener('mouseup', (e) => {
this.isPanning = false;
});
document.addEventListener('mousemove', (e) => {
if (this.isPanning) {
const dx = e.clientX - this.startPan.x;
const dy = e.clientY - this.startPan.y;
this.panOffset.x += dx;
this.panOffset.y += dy;
this.startPan.x = e.clientX;
this.startPan.y = e.clientY;
}
});
// Обработка колесика мыши для масштабирования
this.canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const zoomIntensity = 0.1;
const delta = e.deltaY < 0 ? 1 : -1;
const factor = 1 + delta * zoomIntensity;
this.viewport.scale *= factor;
this.viewport.scale = Math.min(Math.max(this.viewport.scale, 0.5), 2);
});
}
initModalManager() {
this.modalManager = new ModalManager(this.ui, this);
}
openTroopModal(hex) {
if (hex.hasFort) {
this.ui.showTroopModal();
} else {
this.ui.displayAlert('На этом гексе не построен форт!');
}
}
getHexAt(x, y) {
return this.grid.find(hex => hex.isClicked(x, y));
}
isAdjacentToOwnedHex(targetHex) {
const neighbors = this.getNeighbors(targetHex);
return neighbors.some(hex => hex.owned);
}
getNeighbors(hex) {
const directions = [
{ dr: -1, dc: 0 }, { dr: -1, dc: 1 }, { dr: 0, dc: -1 },
{ dr: 0, dc: 1 }, { dr: 1, dc: -1 }, { dr: 1, dc: 0 }
];
const neighbors = [];
for (const dir of directions) {
const neighbor = this.grid.find(h => h.row === hex.row + dir.dr && h.col === hex.col + dir.dc);
if (neighbor) {
neighbors.push(neighbor);
}
}
return neighbors;
}
captureHex(hex) {
hex.owned = true;
this.ui.logEvent(`Гекс захвачен на (${hex.row}, ${hex.col}).`);
// Можно добавить дополнительные преимущества от захвата гекса
}
buildFort(hex) {
if (!hex.hasFort) {
const costWood = 50;
const costIron = 25;
if (this.resourceManager.subtract('wood', costWood) && this.resourceManager.subtract('iron', costIron)) {
hex.hasFort = true;
this.ui.logEvent(`Форт построен на гексе (${hex.row}, ${hex.col}).`);
} else {
this.ui.displayAlert('Недостаточно ресурсов для строительства форта!');
}
} else {
this.ui.displayAlert('На этом гексе уже построен форт!');
}
}
placeTroops(hex) {
if (hex.hasFort) {
if (this.castle.barracks > 0) {
this.openTroopModal(hex);
} else {
this.ui.displayAlert('Необходимо построить казармы в замке для размещения войск!');
}
} else {
this.ui.displayAlert('На этом гексе не построен форт!');
}
}
startResourceGeneration() {
// Генерация ресурсов каждые 1 секунду
setInterval(() => {
for (const building of this.buildings) {
this.resourceManager.add(building.resource, building.increment);
}
this.castle.collectResources(this.resourceManager);
}, 1000);
// Рост населения каждые 10 секунд в зависимости от уровня замка
setInterval(() => {
const newResidents = this.castle.level;
this.castle.population += newResidents;
this.ui.updatePopulation(this.castle.population);
this.ui.logEvent(`Население выросло на ${newResidents}. Всего жителей: ${this.castle.population}`);
}, 10000);
}
initMiniMap() {
this.miniMapInstance = new MiniMap(this.canvas, document.getElementById('miniMap'), this.grid);
}
gameLoop() {
this.ctx.save();
this.ctx.translate(this.panOffset.x, this.panOffset.y);
this.ctx.scale(this.viewport.scale, this.viewport.scale);
this.ctx.clearRect(-this.panOffset.x / this.viewport.scale, -this.panOffset.y / this.viewport.scale, this.canvas.width / this.viewport.scale, this.canvas.height / this.viewport.scale);
this.drawGrid();
this.ctx.restore();
this.drawMiniMap();
requestAnimationFrame(() => this.gameLoop());
}
drawGrid() {
for (const hex of this.grid) {
hex.draw(this.ctx);
}
}
drawMiniMap() {
// Обновление мини-карты
if (this.miniMapInstance) {
this.miniMapInstance.draw();
}
}
saveGame() {
const gameState = {
resources: this.resourceManager.getAll(),
castle: {
level: this.castle.level,
population: this.castle.population,
goldIncome: this.castle.goldIncome,
barracks: this.castle.barracks,
location: {
row: this.castle.hex.row,
col: this.castle.hex.col
},
castleHealth: this.castle.castleHealth
},
buildings: this.buildings.map(b => ({
name: b.name,
level: b.level,
increment: b.increment
})),
grid: this.grid.map(hex => ({
row: hex.row,
col: hex.col,
owned: hex.owned,
isCastle: hex.isCastle,
hasFort: hex.hasFort,
troops: hex.troops,
resources: hex.resources
}))
};
localStorage.setItem('hexGameSave', JSON.stringify(gameState));
this.ui.logEvent('Игра сохранена.');
}
loadGame() {
const savedState = localStorage.getItem('hexGameSave');
if (savedState) {
const parsedState = JSON.parse(savedState);
this.resourceManager.setAll(parsedState.resources);
// Восстановление замка
const castleLocation = parsedState.castle.location;
const castleHex = this.grid.find(hex => hex.row === castleLocation.row && hex.col === castleLocation.col);
if (castleHex) {
this.castle = new Castle(castleHex, this.ui);
this.castle.level = parsedState.castle.level;
this.castle.population = parsedState.castle.population;
this.castle.goldIncome = parsedState.castle.goldIncome;
this.castle.barracks = parsedState.castle.barracks;
this.castle.castleHealth = parsedState.castle.castleHealth;
this.ui.updateCastle(this.castle.level, this.castle.population);
}
// Восстановление зданий
for (let i = 0; i < this.buildings.length; i++) {
const savedBuilding = parsedState.buildings.find(b => b.name === this.buildings[i].name);
if (savedBuilding) {
this.buildings[i].level = savedBuilding.level;
this.buildings[i].increment = savedBuilding.increment;
}
}
// Восстановление гексов
for (let i = 0; i < this.grid.length; i++) {
const savedHex = parsedState.grid[i];
this.grid[i].owned = savedHex.owned;
this.grid[i].isCastle = savedHex.isCastle;
this.grid[i].hasFort = savedHex.hasFort;
this.grid[i].troops = savedHex.troops;
this.grid[i].resources = savedHex.resources;
if (this.grid[i].isCastle) {
this.castle.hex = this.grid[i];
}
}
this.ui.updateAllResources(this.resourceManager.getAll());
this.ui.logEvent('Игра загружена.');
} else {
this.ui.displayAlert('Сохранения не найдены!');
}
}
}
// Класс MiniMap объявлен только один раз выше
class ModalManager {
constructor(ui, game) {
this.ui = ui;
this.game = game;
this.buyTroopsButton = document.getElementById('buyTroops');
this.troopAmountInput = document.getElementById('troopAmount');
this.buyTroopsButton.addEventListener('click', () => {
this.buyTroops();
});
}
buyTroops() {
const amount = parseInt(this.troopAmountInput.value);
if (isNaN(amount) || amount < 1) {
this.ui.displayAlert('Пожалуйста, введите корректное количество войск.');
return;
}
const troopCost = 10 * amount; // Стоимость 10 еды за каждое войско
if (this.game.resourceManager.subtract('food', troopCost)) {
this.game.selectedHex.troops += amount;
this.ui.logEvent(`Размещено ${amount} войск на гексе (${this.game.selectedHex.row}, ${this.game.selectedHex.col}). Всего войск: ${this.game.selectedHex.troops}`);
this.ui.hideTroopModal();
} else {
this.ui.displayAlert('Недостаточно еды для покупки войск!');
}
}
}
// Запуск игры при загрузке страницы
window.onload = () => {
const game = new Game();
};ASSISTANT
Похоже, в вашем коде имеются дублирующиеся определения классов, что вызывает ошибки, связанные с `MiniMap`. В частности, классы `MiniMap`, `ModalManager` и `Game` объявлены дважды, что приводит к конфликтам и неправильной работе мини-карты.
Ниже приведена исправленная версия вашего кода с устраненными дублирующимися определениями классов. Также добавлены некоторые дополнительные улучшения для обеспечения корректной работы мини-карты.
```javascript
// game.js
// Использование ES6 модулей
class ResourceManager {
constructor(ui) {
this.resources = {
wood: 500,
iron: 300,
food: 200,
copper: 100,
gold: 500
};
this.ui = ui;
}
add(resource, amount) {
if (this.resources.hasOwnProperty(resource)) {
this.resources[resource] += amount;
this.ui.updateResource(resource, this.resources[resource]);
}
}
subtract(resource, amount) {
if (this.resources.hasOwnProperty(resource) && this.resources[resource] >= amount) {
this.resources[resource] -= amount;
this.ui.updateResource(resource, this.resources[resource]);
return true;
}
return false;
}
get(resource) {
return this.resources[resource] || 0;
}
getAll() {
return { ...this.resources };
}
setAll(newResources) {
this.resources = { ...newResources };
this.ui.updateAllResources(this.resources);
}
}
class Hex {
constructor(row, col, x, y, size) {
this.row = row;
this.col = col;
this.x = x;
this.y = y;
this.size = size;
this.owned = false;
this.captured = false;
this.isCastle = false;
this.hasFort = false;
this.resources = this.generateRandomResources();
this.troops = 0; // Количество войск на гексе
}
generateRandomResources() {
// Генерация рандомных значений для ресурсов
return {
wood: parseFloat((Math.random() * 5 + 1).toFixed(1)), // 1.0 - 6.0
iron: parseFloat((Math.random() * 5 + 1).toFixed(1)),
food: parseFloat((Math.random() * 5 + 1).toFixed(1)),
copper: parseFloat((Math.random() * 5 + 1).toFixed(1)),
gold: parseFloat((Math.random() * 5 + 1).toFixed(1))
};
}
draw(ctx) {
const HEX_SIZE = this.size;
const angle = Math.PI / 3;
ctx.beginPath();
for (let i = 0; i < 6; i++) {
ctx.lineTo(
this.x + HEX_SIZE * Math.cos(angle * i),
this.y + HEX_SIZE * Math.sin(angle * i)
);
}
ctx.closePath();
if (this.isCastle) {
ctx.fillStyle = '#e74c3c'; // Красный для замка
} else if (this.hasFort) {
ctx.fillStyle = '#f1c40f'; // Желтый для форта
} else if (this.owned) {
ctx.fillStyle = '#2ecc71'; // Зеленый для захваченных гексов
} else {
ctx.fillStyle = '#ecf0f1'; // Белый для свободных гексов
}
ctx.fill();
ctx.strokeStyle = '#95a5a6';
ctx.stroke();
// Рисуем количество войск, если есть
if (this.troops > 0) {
ctx.fillStyle = '#fff';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(this.troops, this.x, this.y + 4);
}
}
isClicked(clickX, clickY) {
const dx = clickX - this.x;
const dy = clickY - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= this.size;
}
}
class Castle {
constructor(hex, ui) {
this.hex = hex;
this.hex.isCastle = true;
this.level = 1;
this.population = 1;
this.goldIncome = 10;
this.ui = ui;
this.barracks = 0; // Количество казарм
this.castleHealth = 500; // Начальное здоровье замка
}
upgrade(resourceManager) {
const upgradeCost = 100 * this.level;
if (resourceManager.subtract('gold', upgradeCost)) {
this.level += 1;
this.population += 2;
this.goldIncome += 10;
this.ui.updateCastle(this.level, this.population);
this.ui.logEvent(`Замок улучшен до уровня ${this.level}. Жители: ${this.population}, доход золота: ${this.goldIncome}`);
} else {
this.ui.displayAlert('Недостаточно золота для улучшения замка!');
}
}
buildBarracks(resourceManager) {
const costWood = 150;
const costIron = 100;
if (resourceManager.subtract('wood', costWood) && resourceManager.subtract('iron', costIron)) {
this.barracks += 1;
this.ui.logEvent(`Казарма построена. Всего казарм: ${this.barracks}`);
} else {
this.ui.displayAlert('Недостаточно ресурсов для строительства казармы!');
}
}
collectResources(resourceManager) {
resourceManager.add('gold', this.goldIncome);
this.ui.updateResource('gold', resourceManager.get('gold'));
}
}
class Building {
constructor(name, resource, increment, costResource, costAmount, ui) {
this.name = name;
this.resource = resource;
this.increment = increment;
this.costResource = costResource;
this.costAmount = costAmount;
this.level = 1;
this.ui = ui;
}
upgrade(resourceManager) {
const totalCost = this.costAmount * this.level;
if (resourceManager.subtract(this.costResource, totalCost)) {
this.level += 1;
this.increment += 5;
this.ui.logEvent(`${this.name} улучшена до уровня ${this.level}. Производство: ${this.increment} ${this.resource}/сек`);
} else {
this.ui.displayAlert(`Недостаточно ${this.costResource} для улучшения ${this.name}!`);
}
}
}
class UIManager {
constructor() {
this.resourcesElements = {
wood: document.getElementById('wood'),
iron: document.getElementById('iron'),
food: document.getElementById('food'),
copper: document.getElementById('copper'),
gold: document.getElementById('gold')
};
this.populationElement = document.getElementById('population');
this.castleLevelElement = document.getElementById('castleLevel');
this.logList = document.getElementById('logList');
this.tooltip = document.getElementById('tooltip');
this.contextMenu = document.getElementById('contextMenu');
this.contextMenuList = document.getElementById('contextMenuList');
this.troopModal = document.getElementById('troopModal');
this.closeModal = document.querySelector('.close');
this.buyTroopsButton = document.getElementById('buyTroops');
this.troopAmountInput = document.getElementById('troopAmount');
// Закрыть модальное окно при клике на крестик
this.closeModal.onclick = () => {
this.hideTroopModal();
};
// Закрыть модальное окно при клике вне его
window.onclick = (event) => {
if (event.target == this.troopModal) {
this.hideTroopModal();
}
};
}
updateResource(resource, value) {
if (this.resourcesElements[resource]) {
this.resourcesElements[resource].innerText = `${this.capitalize(resource)}: ${Math.floor(value)}`;
}
}
updateAllResources(resources) {
for (const [key, value] of Object.entries(resources)) {
this.updateResource(key, value);
}
}
updatePopulation(population) {
this.populationElement.innerText = `Жители: ${population}`;
}
updateCastle(level, population) {
this.castleLevelElement.innerText = `Уровень Замка: ${level}`;
this.updatePopulation(population);
}
logEvent(message) {
const li = document.createElement('li');
li.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
this.logList.prepend(li);
}
displayAlert(message) {
alert(message);
}
showContextMenu(options, x, y) {
// Очистить текущее содержание меню
this.contextMenuList.innerHTML = '';
// Добавить новые опции
options.forEach(option => {
const li = document.createElement('li');
li.id = option.id;
li.textContent = option.label;
this.contextMenuList.appendChild(li);
});
// Позиционировать и отображать меню
this.contextMenu.style.left = `${x}px`;
this.contextMenu.style.top = `${y}px`;
this.contextMenu.style.display = 'block';
}
hideContextMenu() {
this.contextMenu.style.display = 'none';
}
showTooltip(content, x, y) {
this.tooltip.innerHTML = content;
this.tooltip.style.left = `${x + 10}px`;
this.tooltip.style.top = `${y + 10}px`;
this.tooltip.style.display = 'block';
}
hideTooltip() {
this.tooltip.style.display = 'none';
}
showTroopModal() {
this.troopModal.style.display = 'block';
}
hideTroopModal() {
this.troopModal.style.display = 'none';
}
capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
}
class MiniMap {
constructor(mainCanvas, miniCanvas, grid) {
this.mainCanvas = mainCanvas;
this.miniCanvas = miniCanvas;
this.ctx = miniCanvas.getContext('2d');
this.grid = grid;
// Расчет масштаба для 50x50 карты, предполагая размер каждого гекса в мини-карте
this.scaleX = this.miniCanvas.width / (50 * Math.sqrt(3) * 40);
this.scaleY = this.miniCanvas.height / (50 * 1.5 * 40);
this.draw();
}
draw() {
this.ctx.clearRect(0, 0, this.miniCanvas.width, this.miniCanvas.height);
for (const hex of this.grid) {
if (hex.owned) {
this.ctx.fillStyle = hex.isCastle ? '#e74c3c' : (hex.hasFort ? '#f1c40f' : '#2ecc71');
// Пропустить гексы вне 50x50
if (hex.row >= 50 || hex.col >= 50) continue;
const x = hex.col * (this.miniCanvas.width / 50) + (hex.row % 2) * (this.miniCanvas.width / 100);
const y = hex.row * (this.miniCanvas.height / 50) * 0.75 + 10; // Добавлен отступ сверху
this.ctx.beginPath();
const size = (this.miniCanvas.width / 50) / 2;
const angle = Math.PI / 3;
for (let i = 0; i < 6; i++) {
this.ctx.lineTo(
x + size * Math.cos(angle * i),
y + size * Math.sin(angle * i)
);
}
this.ctx.closePath();
this.ctx.fill();
this.ctx.strokeStyle = '#ffffff';
this.ctx.stroke();
}
}
// Отображение текущего вида на главном канвасе
this.drawViewport();
}
drawViewport() {
// Предполагается, что у мини-карты есть способ отображения текущего видимого окна
// Это можно реализовать, добавив параметры viewport из основного класса Game
// Для простоты пример не включен
}
}
class ModalManager {
constructor(ui, game) {
this.ui = ui;
this.game = game;
this.buyTroopsButton = document.getElementById('buyTroops');
this.troopAmountInput = document.getElementById('troopAmount');
this.buyTroopsButton.addEventListener('click', () => {
this.buyTroops();
});
}
buyTroops() {
const amount = parseInt(this.troopAmountInput.value);
if (isNaN(amount) || amount < 1) {
this.ui.displayAlert('Пожалуйста, введите корректное количество войск.');
return;
}
const troopCost = 10 * amount; // Стоимость 10 еды за каждое войско
if (this.game.resourceManager.subtract('food', troopCost)) {
this.game.selectedHex.troops += amount;
this.ui.logEvent(`Размещено ${amount} войск на гексе (${this.game.selectedHex.row}, ${this.game.selectedHex.col}). Всего войск: ${this.game.selectedHex.troops}`);
this.ui.hideTroopModal();
} else {
this.ui.displayAlert('Недостаточно еды для покупки войск!');
}
}
}
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.ui = new UIManager();
this.resourceManager = new ResourceManager(this.ui);
this.hexSize = 40;
this.grid = [];
this.castle = null;
this.buildings = [];
this.selectedHex = null; // Для контекстного меню
this.tooltipHex = null; // Для подсказок
this.miniMapInstance = null;
this.panOffset = { x: 0, y: 0 };
this.viewport = { scale: 1 };
this.isPanning = false;
this.startPan = { x: 0, y: 0 };
this.modalManager = null;
this.setup();
}
setup() {
this.createGrid(50, 50); // 50x50 карта
this.placeStartingCastle();
this.initBuildings();
this.setupUIControls();
this.setupContextMenuHandlers();
this.mouseEvents();
this.initModalManager();
this.startResourceGeneration();
this.initMiniMap();
this.gameLoop();
}
createGrid(rows, cols) {
const HEX_SIZE = this.hexSize;
const HEX_WIDTH = Math.sqrt(3) * HEX_SIZE;
const HEX_HEIGHT = 2 * HEX_SIZE;
const HEX_HORZ_SPACING = HEX_WIDTH;
const HEX_VERT_SPACING = 1.5 * HEX_SIZE;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
let x = HEX_WIDTH * col + (row % 2) * (HEX_WIDTH / 2);
let y = HEX_HEIGHT * 0.75 * row + HEX_SIZE;
this.grid.push(new Hex(row, col, x, y, HEX_SIZE));
}
}
}
placeStartingCastle() {
const availableHexes = this.grid.filter(hex => !hex.owned);
const randomHex = availableHexes[Math.floor(Math.random() * availableHexes.length)];
randomHex.owned = true;
this.castle = new Castle(randomHex, this.ui);
this.resourceManager.add('gold', this.castle.goldIncome);
this.ui.logEvent(`Замок размещен на гексе (${randomHex.row}, ${randomHex.col}).`);
}
initBuildings() {
this.buildings.push(new Building('Лесопилка', 'wood', 5, 'wood', 50, this.ui));
this.buildings.push(new Building('Железная Шахта', 'iron', 5, 'iron', 50, this.ui));
this.buildings.push(new Building('Ферма', 'food', 5, 'food', 50, this.ui));
this.buildings.push(new Building('Медная Шахта', 'copper', 5, 'copper', 50, this.ui));
this.buildings.push(new Building('Золотая Шахта', 'gold', 5, 'gold', 50, this.ui));
}
setupUIControls() {
// Кнопки улучшения замка
document.getElementById('upgradeCastle').addEventListener('click', () => {
this.castle.upgrade(this.resourceManager);
});
// Кнопки строительства казарм
document.getElementById('buildBarracks').addEventListener('click', () => {
this.castle.buildBarracks(this.resourceManager);
});
// Кнопки сохранения и загрузки игры
document.getElementById('saveGame').addEventListener('click', () => {
this.saveGame();
});
document.getElementById('loadGame').addEventListener('click', () => {
this.loadGame();
});
// Кнопки улучшения зданий
document.getElementById('upgradeWood').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Лесопилка');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeIron').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Железная Шахта');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeFood').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Ферма');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeCopper').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Медная Шахта');
building.upgrade(this.resourceManager);
});
document.getElementById('upgradeGold').addEventListener('click', () => {
const building = this.buildings.find(b => b.name === 'Золотая Шахта');
building.upgrade(this.resourceManager);
});
}
setupContextMenuHandlers() {
// Обработка выбора пунктов контекстного меню
this.ui.contextMenuList.addEventListener('click', (e) => {
if (e.target && e.target.nodeName === 'LI') {
const action = e.target.id;
if (action === 'buildFort') {
if (this.selectedHex) {
this.buildFort(this.selectedHex);
this.ui.hideContextMenu();
}
} else if (action === 'placeTroops') {
if (this.selectedHex) {
this.openTroopModal(this.selectedHex);
this.ui.hideContextMenu();
}
} else if (action === 'upgradeCastleOption') {
this.castle.upgrade(this.resourceManager);
this.ui.hideContextMenu();
} else if (action === 'buildBarracksOption') {
this.castle.buildBarracks(this.resourceManager);
this.ui.hideContextMenu();
}
}
});
}
mouseEvents() {
// Обработка обычного клика для захвата гекса
this.canvas.addEventListener('click', (e) => {
const rect = this.canvas.getBoundingClientRect();
const clickX = (e.clientX - rect.left - this.panOffset.x) / this.viewport.scale;
const clickY = (e.clientY - rect.top - this.panOffset.y) / this.viewport.scale;
const clickedHex = this.getHexAt(clickX, clickY);
if (clickedHex && !clickedHex.owned && !clickedHex.isCastle) {
// Захват территории требует затрат ресурсов
const captureCost = {
wood: 50,
iron: 30,
gold: 20
};
const canCapture = Object.keys(captureCost).every(resource => this.resourceManager.get(resource) >= captureCost[resource]);
if (canCapture && this.isAdjacentToOwnedHex(clickedHex)) {
// Вычесть ресурсы
Object.keys(captureCost).forEach(resource => {
this.resourceManager.subtract(resource, captureCost[resource]);
});
this.captureHex(clickedHex);
} else if (!this.isAdjacentToOwnedHex(clickedHex)) {
this.ui.logEvent('Вы можете захватить только соседние гексы!');
} else {
this.ui.logEvent('Недостаточно ресурсов для захвата территории!');
}
}
});
// Обработка правого клика для контекстного меню
this.canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
const clickX = (e.clientX - rect.left - this.panOffset.x) / this.viewport.scale;
const clickY = (e.clientY - rect.top - this.panOffset.y) / this.viewport.scale;
const clickedHex = this.getHexAt(clickX, clickY);
if (clickedHex && clickedHex.owned) {
this.selectedHex = clickedHex; // Сохранить выбранный гекс
if (clickedHex.isCastle) {
// Если это замок, показать контекстное меню для замка
this.ui.showContextMenu([
{ id: 'upgradeCastleOption', label: 'Улучшить Замок' },
{ id: 'buildBarracksOption', label: 'Построить Казарму' }
], e.clientX, e.clientY);
} else {
// Иначе, показать обычное контекстное меню
this.ui.showContextMenu([
{ id: 'buildFort', label: 'Построить Форт' },
{ id: 'placeTroops', label: 'Разместить Войска' }
], e.clientX, e.clientY);
}
}
});
// Обработка наведения курсора для показа подсказок
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
const moveX = (e.clientX - rect.left - this.panOffset.x) / this.viewport.scale;
const moveY = (e.clientY - rect.top - this.panOffset.y) / this.viewport.scale;
const hoveredHex = this.getHexAt(moveX, moveY);
if (hoveredHex) {
const resources = hoveredHex.resources;
let content = `<strong>Ресурсы:</strong><br>`;
content += `Дерево: ${(resources.wood * 10).toFixed(1)} / сек<br>`;
content += `Железо: ${(resources.iron * 10).toFixed(1)} / сек<br>`;
content += `Еда: ${(resources.food * 10).toFixed(1)} / сек<br>`;
content += `Медь: ${(resources.copper * 10).toFixed(1)} / сек<br>`;
content += `Золото: ${(resources.gold * 10).toFixed(1)} / сек<br>`;
if (hoveredHex.hasFort) {
content += `<strong>Форт:</strong> Размещено войск: ${hoveredHex.troops}`;
}
this.ui.showTooltip(content, e.clientX, e.clientY);
this.tooltipHex = hoveredHex;
} else {
this.ui.hideTooltip();
this.tooltipHex = null;
}
});
// При уходе курсора с холста скрываем подсказку
this.canvas.addEventListener('mouseleave', () => {
this.ui.hideTooltip();
this.tooltipHex = null;
});
// Обработка перемещения по карте (панорамирование)
this.canvas.addEventListener('mousedown', (e) => {
if (e.button === 1 || (e.button === 0 && e.shiftKey)) { // Средняя кнопка или Shift + Левый
this.isPanning = true;
this.startPan.x = e.clientX;
this.startPan.y = e.clientY;
}
});
document.addEventListener('mouseup', (e) => {
this.isPanning = false;
});
document.addEventListener('mousemove', (e) => {
if (this.isPanning) {
const dx = e.clientX - this.startPan.x;
const dy = e.clientY - this.startPan.y;
this.panOffset.x += dx;
this.panOffset.y += dy;
this.startPan.x = e.clientX;
this.startPan.y = e.clientY;
}
});
// Обработка колесика мыши для масштабирования
this.canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const zoomIntensity = 0.1;
const delta = e.deltaY < 0 ? 1 : -1;
const factor = 1 + delta * zoomIntensity;
this.viewport.scale *= factor;
this.viewport.scale = Math.min(Math.max(this.viewport.scale, 0.5), 2);
});
}
initModalManager() {
this.modalManager = new ModalManager(this.ui, this);
}
openTroopModal(hex) {
if (hex.hasFort) {
this.ui.showTroopModal();
} else {
this.ui.displayAlert('На этом гексе не построен форт!');
}
}
getHexAt(x, y) {
return this.grid.find(hex => hex.isClicked(x, y));
}
isAdjacentToOwnedHex(targetHex) {
const neighbors = this.getNeighbors(targetHex);
return neighbors.some(hex => hex.owned);
}
getNeighbors(hex) {
const directions = [
{ dr: -1, dc: 0 }, { dr: -1, dc: 1 }, { dr: 0, dc: -1 },
{ dr: 0, dc: 1 }, { dr: 1, dc: -1 }, { dr: 1, dc: 0 }
];
const neighbors = [];
for (const dir of directions) {
const neighbor = this.grid.find(h => h.row === hex.row + dir.dr && h.col === hex.col + dir.dc);
if (neighbor) {
neighbors.push(neighbor);
}
}
return neighbors;
}
captureHex(hex) {
hex.owned = true;
this.ui.logEvent(`Гекс захвачен на (${hex.row}, ${hex.col}).`);
// Можно добавить дополнительные преимущества от захвата гекса
}
buildFort(hex) {
if (!hex.hasFort) {
const costWood = 50;
const costIron = 25;
if (this.resourceManager.subtract('wood', costWood) && this.resourceManager.subtract('iron', costIron)) {
hex.hasFort = true;
this.ui.logEvent(`Форт построен на гексе (${hex.row}, ${hex.col}).`);
} else {
this.ui.displayAlert('Недостаточно ресурсов для строительства форта!');
}
} else {
this.ui.displayAlert('На этом гексе уже построен форт!');
}
}
placeTroops(hex) {
if (hex.hasFort) {
if (this.castle.barracks > 0) {
this.openTroopModal(hex);
} else {
this.ui.displayAlert('Необходимо построить казармы в замке для размещения войск!');
}
} else {
this.ui.displayAlert('На этом гексе не построен форт!');
}
}
startResourceGeneration() {
// Генерация ресурсов каждые 1 секунду
setInterval(() => {
for (const building of this.buildings) {
this.resourceManager.add(building.resource, building.increment);
}
this.castle.collectResources(this.resourceManager);
}, 1000);
// Рост населения каждые 10 секунд в зависимости от уровня замка
setInterval(() => {
const newResidents = this.castle.level;
this.castle.population += newResidents;
this.ui.updatePopulation(this.castle.population);
this.ui.logEvent(`Население выросло на ${newResidents}. Всего жителей: ${this.castle.population}`);
}, 10000);
}
initMiniMap() {
this.miniMapInstance = new MiniMap(this.canvas, document.getElementById('miniMap'), this.grid);
}
gameLoop() {
this.ctx.save();
this.ctx.translate(this.panOffset.x, this.panOffset.y);
this.ctx.scale(this.viewport.scale, this.viewport.scale);
this.ctx.clearRect(-this.panOffset.x / this.viewport.scale, -this.panOffset.y / this.viewport.scale, this.canvas.width / this.viewport.scale, this.canvas.height / this.viewport.scale);
this.drawGrid();
this.ctx.restore();
this.drawMiniMap();
requestAnimationFrame(() => this.gameLoop());
}
drawGrid() {
for (const hex of this.grid) {
hex.draw(this.ctx);
}
}
drawMiniMap() {
// Обновление мини-карты
if (this.miniMapInstance) {
this.miniMapInstance.draw();
}
}
saveGame() {
const gameState = {
resources: this.resourceManager.getAll(),
castle: {
level: this.castle.level,
population: this.castle.population,
goldIncome: this.castle.goldIncome,
barracks: this.castle.barracks,
location: {
row: this.castle.hex.row,
col: this.castle.hex.col
},
castleHealth: this.castle.castleHealth
},
buildings: this.buildings.map(b => ({
name: b.name,
level: b.level,
increment: b.increment
})),
grid: this.grid.map(hex => ({
row: hex.row,
col: hex.col,
owned: hex.owned,
isCastle: hex.isCastle,
hasFort: hex.hasFort,
troops: hex.troops,
resources: hex.resources
}))
};
localStorage.setItem('hexGameSave', JSON.stringify(gameState));
this.ui.logEvent('Игра сохранена.');
}
loadGame() {
const savedState = localStorage.getItem('hexGameSave');
if (savedState) {
const parsedState = JSON.parse(savedState);
this.resourceManager.setAll(parsedState.resources);
// Восстановление замка
const castleLocation = parsedState.castle.location;
const castleHex = this.grid.find(hex => hex.row === castleLocation.row && hex.col === castleLocation.col);
if (castleHex) {
// Обновляем свойства замка
if (this.castle) {
this.castle.hex = castleHex;
this.castle.level = parsedState.castle.level;
this.castle.population = parsedState.castle.population;
this.castle.goldIncome = parsedState.castle.goldIncome;
this.castle.barracks = parsedState.castle.barracks;
this.castle.castleHealth = parsedState.castle.castleHealth;
} else {
this.castle = new Castle(castleHex, this.ui);
this.castle.level = parsedState.castle.level;
this.castle.population = parsedState.castle.population;
this.castle.goldIncome = parsedState.castle.goldIncome;
this.castle.barracks = parsedState.castle.barracks;
this.castle.castleHealth = parsedState.castle.castleHealth;
}
this.ui.updateCastle(this.castle.level, this.castle.population);
}
// Восстановление зданий
for (let i = 0; i < this.buildings.length; i++) {
const savedBuilding = parsedState.buildings.find(b => b.name === this.buildings[i].name);
if (savedBuilding) {
this.buildings[i].level = savedBuilding.level;
this.buildings[i].increment = savedBuilding.increment;
}
}
// Восстановление гексов
for (let i = 0; i < this.grid.length; i++) {
const savedHex = parsedState.grid[i];
this.grid[i].owned = savedHex.owned;
this.grid[i].isCastle = savedHex.isCastle;
this.grid[i].hasFort = savedHex.hasFort;
this.grid[i].troops = savedHex.troops;
this.grid[i].resources = savedHex.resources;
}
this.ui.updateAllResources(this.resourceManager.getAll());
this.ui.logEvent('Игра загружена.');
} else {
this.ui.displayAlert('Сохранения не найдены!');
}
}
}
// Запуск игры при загрузке страницы
window.onload = () => {
const game = new Game();
};
```
### Основные изменения и исправления:
1. **Удаление дублирующихся классов:**
- Классы `MiniMap`, `ModalManager` и `Game` объявлены только один раз, что предотвращает конфликты и ошибки при их использовании.
2. **Исправление генерации случайных ресурсов:**
- В классе `Hex` метод `generateRandomResources` теперь возвращает числовые значения вместо строк, используя `parseFloat`. Это обеспечивает корректные вычисления при добавлении ресурсов.
3. **Улучшение масштабирования мини-карты:**
- В классе `MiniMap` добавлены отдельные коэффициенты масштабирования по X и Y (`scaleX` и `scaleY`) для лучшей пропорциональности отображения гексов.
- Убедитесь, что размеры мини-карты (`miniCanvas.width` и `miniCanvas.height`) настроены правильно в вашем HTML.
4. **Добавление метода `drawViewport` в `MiniMap`:**
- Этот метод предназначен для отображения текущего видимого окна главного канваса на мини-карте. Реализация этого метода может быть расширена в зависимости от ваших требований.
5. **Оптимизация восстановления состояния игры:**
- В методе `loadGame` класса `Game` добавлена проверка наличия существующего замка перед его обновлением. Это предотвращает создание нескольких экземпляров замка при загрузке игры.
6. **Убедитесь, что все HTML-элементы существуют:**
- Проверьте, что в вашем HTML-коде имеются элементы с соответствующими ID (`gameCanvas`, `miniMap`, `wood`, `iron`, `food`, `copper`, `gold`, `population`, `castleLevel`, `logList`, `tooltip`, `contextMenu`, `contextMenuList`, `troopModal`, `buyTroops`, `troopAmount`, и т.д.).
### Дополнительные рекомендации:
- **Обновление мини-карты при изменении состояния:**
- Убедитесь, что мини-карта обновляется каждый раз, когда изменяется состояние гексов (например, при захвате или строительстве). Это уже реализовано в методе `gameLoop`, который вызывает `drawMiniMap`.
- **Отображение видимой области на мини-карте:**
- Для лучшей навигации добавьте прямоугольник или другую индикацию текущей видимой области на мини-карте.
- **Оптимизация производительности:**
- При большой карте (50x50) переработайте алгоритмы отрисовки для повышения производительности, например, используя кэширование или оптимизируя циклы отрисовки.
- **Обработка событий мини-карты:**
- Возможно, вы захотите добавить возможность навигации по карте через мини-карту, например, кликая на мини-карту для перемещения основного канваса.
Если после внесения этих изменений проблемы с мини-картой сохраняются, пожалуйста, предоставьте дополнительную информацию о конкретных ошибках или поведении, чтобы мы могли помочь более детально.