Files
sillytavern-rts-mode/ui/MapCanvas.js
T
2025-08-03 17:35:34 -07:00

318 lines
9.4 KiB
JavaScript

/**
* Enhanced map canvas for RTS mode with terrain, units, and buildings
*/
export class RTSMapCanvas {
constructor(canvasId = 'rts-game-map') {
this.canvas = document.getElementById(canvasId);
if (!this.canvas) {
this.canvas = document.createElement('canvas');
this.canvas.id = canvasId;
}
this.ctx = this.canvas.getContext('2d');
this.zoom = 1;
this.offsetX = 0;
this.offsetY = 0;
this.selectedUnit = null;
this.selectedBuilding = null;
this.initializeCanvas();
this.setupEventListeners();
}
initializeCanvas() {
this.canvas.width = 800;
this.canvas.height = 600;
this.drawTerrain();
this.drawGrid();
}
setupEventListeners() {
this.canvas.addEventListener('click', (e) => this.handleClick(e));
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
}
drawTerrain() {
const ctx = this.ctx;
// Clear canvas
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw terrain features
this.drawForests();
this.drawMountains();
this.drawRivers();
this.drawSettlement();
}
drawGrid() {
const ctx = this.ctx;
ctx.strokeStyle = '#334155';
ctx.lineWidth = 0.5;
ctx.globalAlpha = 0.3;
const gridSize = 40;
// Vertical lines
for (let x = 0; x <= this.canvas.width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, this.canvas.height);
ctx.stroke();
}
// Horizontal lines
for (let y = 0; y <= this.canvas.height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(this.canvas.width, y);
ctx.stroke();
}
ctx.globalAlpha = 1;
}
drawForests() {
const ctx = this.ctx;
ctx.fillStyle = '#22c55e';
// Forest patches
const forests = [
{ x: 100, y: 100, radius: 60 },
{ x: 600, y: 150, radius: 80 },
{ x: 200, y: 400, radius: 50 },
{ x: 650, y: 450, radius: 70 }
];
forests.forEach(forest => {
ctx.beginPath();
ctx.arc(forest.x, forest.y, forest.radius, 0, Math.PI * 2);
ctx.fill();
// Add tree symbols
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const treeX = forest.x + Math.cos(angle) * (forest.radius * 0.7);
const treeY = forest.y + Math.sin(angle) * (forest.radius * 0.7);
ctx.fillStyle = '#16a34a';
ctx.fillRect(treeX - 2, treeY - 2, 4, 4);
}
ctx.fillStyle = '#22c55e';
});
}
drawMountains() {
const ctx = this.ctx;
ctx.fillStyle = '#64748b';
const mountains = [
{ x: 300, y: 80, width: 100, height: 80 },
{ x: 500, y: 300, width: 120, height: 100 }
];
mountains.forEach(mountain => {
ctx.beginPath();
ctx.moveTo(mountain.x, mountain.y + mountain.height);
ctx.lineTo(mountain.x + mountain.width / 2, mountain.y);
ctx.lineTo(mountain.x + mountain.width, mountain.y + mountain.height);
ctx.closePath();
ctx.fill();
});
}
drawRivers() {
const ctx = this.ctx;
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(0, 200);
ctx.quadraticCurveTo(200, 180, 400, 220);
ctx.quadraticCurveTo(600, 260, 800, 240);
ctx.stroke();
}
drawSettlement() {
const ctx = this.ctx;
const centerX = 400;
const centerY = 350;
// Town hall (main building)
ctx.fillStyle = '#8b5cf6';
ctx.fillRect(centerX - 20, centerY - 20, 40, 40);
// Surrounding buildings
const buildings = [
{ x: centerX - 60, y: centerY - 10, type: 'barracks', color: '#ef4444' },
{ x: centerX + 40, y: centerY - 10, type: 'storage', color: '#f59e0b' },
{ x: centerX - 10, y: centerY - 60, type: 'farm', color: '#22c55e' },
{ x: centerX - 10, y: centerY + 40, type: 'market', color: '#3b82f6' }
];
buildings.forEach(building => {
ctx.fillStyle = building.color;
ctx.fillRect(building.x - 15, building.y - 15, 30, 30);
});
// Draw units around settlement
this.drawUnits();
}
drawUnits() {
const ctx = this.ctx;
const centerX = 400;
const centerY = 350;
const units = [
{ x: centerX - 80, y: centerY + 20, type: 'warrior' },
{ x: centerX - 70, y: centerY + 35, type: 'warrior' },
{ x: centerX + 60, y: centerY + 25, type: 'archer' },
{ x: centerX + 20, y: centerY - 80, type: 'cavalry' }
];
units.forEach(unit => {
if (unit.type === 'warrior') {
ctx.fillStyle = '#dc2626';
ctx.beginPath();
ctx.arc(unit.x, unit.y, 6, 0, Math.PI * 2);
ctx.fill();
} else if (unit.type === 'archer') {
ctx.fillStyle = '#059669';
ctx.fillRect(unit.x - 5, unit.y - 5, 10, 10);
} else if (unit.type === 'cavalry') {
ctx.fillStyle = '#7c3aed';
ctx.beginPath();
ctx.moveTo(unit.x, unit.y - 8);
ctx.lineTo(unit.x - 6, unit.y + 4);
ctx.lineTo(unit.x + 6, unit.y + 4);
ctx.closePath();
ctx.fill();
}
});
}
handleClick(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
console.log(`Map clicked at: ${x}, ${y}`);
// Check if clicked on a unit or building
// This is a simplified example - in a real game you'd have proper hit detection
this.showMapInfo(x, y);
}
handleMouseMove(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Update cursor based on what's under it
this.canvas.style.cursor = 'default';
// Check for interactive elements
if (this.isNearSettlement(x, y)) {
this.canvas.style.cursor = 'pointer';
}
}
isNearSettlement(x, y) {
const centerX = 400;
const centerY = 350;
const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
return distance < 100;
}
showMapInfo(x, y) {
const overlay = document.getElementById('rts-map-overlay');
if (overlay) {
if (this.isNearSettlement(x, y)) {
overlay.innerHTML = `
<div style="position: absolute; top: ${y + 10}px; left: ${x + 10}px;
background: rgba(0,0,0,0.8); color: white; padding: 8px;
border-radius: 4px; font-size: 12px; pointer-events: auto;">
<strong>Main Settlement</strong><br>
Population: 150<br>
Defense: High<br>
<button onclick="this.parentElement.remove()" style="margin-top: 4px;">Close</button>
</div>
`;
} else {
overlay.innerHTML = '';
}
}
}
zoomIn() {
this.zoom = Math.min(this.zoom * 1.2, 3);
this.redraw();
}
zoomOut() {
this.zoom = Math.max(this.zoom / 1.2, 0.5);
this.redraw();
}
centerMap() {
this.offsetX = 0;
this.offsetY = 0;
this.zoom = 1;
this.redraw();
}
redraw() {
this.ctx.save();
this.ctx.scale(this.zoom, this.zoom);
this.ctx.translate(this.offsetX, this.offsetY);
this.drawTerrain();
this.drawGrid();
this.ctx.restore();
}
updateFromGameState(gameState) {
// Update the map based on game state changes
console.log('Updating map from game state:', gameState);
this.redraw();
}
}
/**
* Legacy function for backward compatibility
* @param {HTMLElement} rootEl - The root element to append to (optional).
* @returns {HTMLCanvasElement}
*/
export function createMapCanvas(rootEl) {
const canvas = document.createElement('canvas');
canvas.id = 'rts-map-canvas';
canvas.width = 640;
canvas.height = 640;
const ctx = canvas.getContext('2d');
ctx.strokeStyle = '#444';
ctx.lineWidth = 1;
for (let i = 0; i <= canvas.width; i += 40) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, canvas.height);
ctx.stroke();
}
for (let i = 0; i <= canvas.height; i += 40) {
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(canvas.width, i);
ctx.stroke();
}
if (rootEl) {
rootEl.appendChild(canvas);
}
return canvas;
}