PyCatan-AI / pycatan /static /js /board.js
EZTIME2025
adding pots
b4a2ca4
// CatanBoard class for managing the game board visualization
class CatanBoard {
constructor() {
this.svg = document.getElementById('catan-board');
this.hexRadius = 45;
this.centerX = 400;
this.centerY = 300;
this.zoomLevel = 1;
this.panX = 0;
this.panY = 0;
this.isDragging = false;
this.lastMouseX = 0;
this.lastMouseY = 0;
this.showVertices = true;
// Board mapping from server
this.boardMapping = null;
this.vertices = [];
this.init();
}
async init() {
this.setupEventListeners();
// Load board mapping from server
await this.loadBoardMapping();
if (this.boardMapping && this.boardMapping.points) {
console.log("Using server-provided board mapping for vertices");
this.generateVerticesFromServer();
} else {
// Generate vertices derived directly from hex geometry
// This ensures perfect visual alignment
this.generateVerticesFromHexes();
}
this.createBoard();
}
async loadBoardMapping() {
try {
const response = await fetch('/api/board_mapping');
this.boardMapping = await response.json();
console.log('Board mapping loaded from server:', this.boardMapping);
} catch (error) {
console.error('Failed to load board mapping:', error);
// Fallback to default if server fails
this.boardMapping = null;
}
}
setupEventListeners() {
// Zoom and pan events
this.svg.addEventListener('wheel', (e) => this.handleZoom(e));
this.svg.addEventListener('mousedown', (e) => this.startDrag(e));
this.svg.addEventListener('mousemove', (e) => this.handleDrag(e));
this.svg.addEventListener('mouseup', () => this.endDrag());
this.svg.addEventListener('mouseleave', () => this.endDrag());
}
// Convert hex coordinates to pixels
hexToPixel(q, r) {
const x = this.hexRadius * (3/2 * q);
const y = this.hexRadius * (Math.sqrt(3)/2 * q + Math.sqrt(3) * r);
return {
x: this.centerX + x,
y: this.centerY + y
};
}
// Get hexagon vertices
getHexagonVertices(q, r) {
const center = this.hexToPixel(q, r);
const vertices = [];
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i;
const x = center.x + this.hexRadius * Math.cos(angle);
const y = center.y + this.hexRadius * Math.sin(angle);
vertices.push({x: x, y: y});
}
return vertices;
}
generateVerticesFromHexes() {
console.log('Generating vertices derived from hex geometry...');
this.vertices = [];
const uniqueVerticesMap = new Map(); // To prevent duplicates
// Get hex data (from game state, board mapping, or default)
let hexes;
if (this.currentGameState && this.currentGameState.hexes && this.currentGameState.hexes.length > 0) {
hexes = this.currentGameState.hexes;
} else if (this.boardMapping && this.boardMapping.hexes) {
hexes = this.boardMapping.hexes;
} else {
hexes = GAMEDATA.hexes;
}
hexes.forEach(hex => {
// Get the 6 corners of the current hex
const corners = this.getHexagonVertices(hex.q, hex.r);
corners.forEach(corner => {
// Create unique key based on position (rounded to handle floating point)
const keyX = Math.round(corner.x);
const keyY = Math.round(corner.y);
const key = `${keyX},${keyY}`;
if (!uniqueVerticesMap.has(key)) {
uniqueVerticesMap.set(key, {
x: corner.x,
y: corner.y,
adjacent_hexes: [hex.id]
});
} else {
// If point exists, add hex to its adjacent list
const entry = uniqueVerticesMap.get(key);
if (!entry.adjacent_hexes.includes(hex.id)) {
entry.adjacent_hexes.push(hex.id);
}
}
});
});
// Convert map to array
let tempVertices = Array.from(uniqueVerticesMap.values());
// Sort: First by Y (rows), then by X (columns)
// This attempts to match the server's ID generation order (row by row, left to right)
tempVertices.sort((a, b) => {
// Use a tolerance for Y comparison to group vertices into rows
if (Math.abs(a.y - b.y) > 10) return a.y - b.y;
return a.x - b.x;
});
// Create final structure with IDs
this.vertices = tempVertices.map((v, index) => ({
id: index + 1, // Renumber 1-54
x: v.x,
y: v.y,
game_coords: [], // Not critical for display
adjacent_points: [], // Will be calculated if needed
adjacent_hexes: v.adjacent_hexes,
buildings: []
}));
console.log(`Generated ${this.vertices.length} vertices aligned to hex corners`);
}
generateVerticesFromServer() {
// Generate vertices using the server-provided board mapping
if (!this.boardMapping || !this.boardMapping.points) {
console.error('No board mapping available from server, using fallback');
// Create a fallback basic vertex layout
this.generateFallbackVertices();
return;
}
this.vertices = [];
// Use the server-provided point data
for (const pointData of this.boardMapping.points) {
const vertex = {
id: pointData.id, // Point ID (1-54)
x: pointData.x, // Pixel coordinates from server
y: pointData.y,
game_coords: pointData.game_coords, // [row, col] for debugging
adjacent_points: pointData.adjacent_points || [],
adjacent_hexes: pointData.adjacent_hexes || [],
buildings: [] // Will be populated when buildings are added
};
this.vertices.push(vertex);
}
console.log(`Generated ${this.vertices.length} vertices from server data`);
}
generateFallbackVertices() {
// Generate basic vertices when server mapping is not available
console.log('Using fallback vertex generation');
this.vertices = [];
// Create a basic grid of vertices for testing
let vertexId = 1;
const rows = [7, 9, 11, 11, 9, 7]; // Standard Catan point distribution
for (let row = 0; row < rows.length; row++) {
const rowWidth = rows[row];
for (let col = 0; col < rowWidth; col++) {
// Simple grid positioning
const offsetX = -(rowWidth - 1) * this.hexRadius * 0.5 * 0.75;
const x = this.centerX + offsetX + col * this.hexRadius * 0.75;
const y = this.centerY + (row - 2.5) * this.hexRadius * 0.866;
this.vertices.push({
id: vertexId,
x: x,
y: y,
game_coords: [row, col],
adjacent_points: [],
adjacent_hexes: [],
buildings: []
});
vertexId++;
}
}
console.log(`Generated ${this.vertices.length} fallback vertices`);
}
// Get vertex by point ID
getVertexByPointId(pointId) {
return this.vertices.find(v => v.id === pointId);
}
// Get vertex by coordinates (for backward compatibility)
getVertexByCoords(x, y, tolerance = 20) {
return this.vertices.find(v => {
const dx = v.x - x;
const dy = v.y - y;
return Math.sqrt(dx * dx + dy * dy) < tolerance;
});
}
createBoard() {
// Create the game board with hexes and vertices
console.log('Creating game board...');
// Clear any existing content
this.svg.innerHTML = '';
// Create hexes first (either from server data or fallback)
this.createHexes();
// Create vertices
this.createVertices();
// Set initial transform
this.updateTransform();
console.log('Game board created successfully');
}
createHexes() {
// Create hexes on the board
// Use server data if available, otherwise fallback to GAMEDATA
let hexData;
if (this.currentGameState && this.currentGameState.hexes && this.currentGameState.hexes.length > 0) {
hexData = this.currentGameState.hexes;
console.log('Using hexes from game state');
} else if (this.boardMapping && this.boardMapping.hexes) {
hexData = this.boardMapping.hexes;
console.log('Using hexes from board mapping');
} else {
hexData = GAMEDATA.hexes;
console.log('Using fallback hex data from GAMEDATA');
}
hexData.forEach(hex => {
this.createHex(hex);
});
}
createBoard() {
// Clear existing content
this.svg.innerHTML = '';
// Determine which hex data to use
let hexData;
if (this.currentGameState && this.currentGameState.hexes && this.currentGameState.hexes.length > 0) {
hexData = this.currentGameState.hexes;
console.log(`Using server hexes: ${hexData.length} hexes`);
} else {
hexData = GAMEDATA.hexes;
console.log(`Using default GAMEDATA hexes: ${hexData.length} hexes`);
}
// Create hexes
hexData.forEach(hex => {
this.createHex(hex);
});
// Create vertices
this.createVertices();
// Create buildings only if we don't have current game state
// (when called directly, not from updateFromGameState)
if (!this.currentGameState) {
// Create settlements from GAMEDATA (fallback)
GAMEDATA.settlements.forEach(settlement => {
this.createSettlement(settlement);
});
// Create cities from GAMEDATA (fallback)
GAMEDATA.cities.forEach(city => {
this.createCity(city);
});
// Create roads from GAMEDATA (fallback)
GAMEDATA.roads.forEach(road => {
this.createRoad(road);
});
}
this.updateTransform();
}
createHex(hex) {
const vertices = this.getHexagonVertices(hex.q, hex.r);
const center = this.hexToPixel(hex.q, hex.r);
// Calculate bounding rectangle for the hex
const minX = Math.min(...vertices.map(v => v.x));
const maxX = Math.max(...vertices.map(v => v.x));
const minY = Math.min(...vertices.map(v => v.y));
const maxY = Math.max(...vertices.map(v => v.y));
// Create group for hex
const hexGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
// Create clipPath for hex
const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
clipPath.setAttribute('id', `clip-${hex.id}`);
const pathData = vertices.map((vertex, index) => {
const command = index === 0 ? 'M' : 'L';
return `${command} ${vertex.x.toFixed(2)} ${vertex.y.toFixed(2)}`;
}).join(' ') + ' Z';
const clipPathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
clipPathElement.setAttribute('d', pathData);
clipPath.appendChild(clipPathElement);
// Add clipPath to defs
let defs = this.svg.querySelector('defs');
if (!defs) {
defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
this.svg.appendChild(defs);
}
defs.appendChild(clipPath);
// Create image that fills the hex
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
image.setAttribute('href', `static/images/${RESOURCE_FILES[hex.type]}`);
image.setAttribute('x', minX);
image.setAttribute('y', minY);
image.setAttribute('width', maxX - minX);
image.setAttribute('height', maxY - minY);
image.setAttribute('preserveAspectRatio', 'xMidYMid slice');
image.setAttribute('clip-path', `url(#clip-${hex.id})`);
hexGroup.appendChild(image);
// Create hex element (for borders only)
const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pathElement.setAttribute('d', pathData);
pathElement.setAttribute('class', `hexagon hex-${hex.type}`);
pathElement.setAttribute('data-hex-id', hex.id);
pathElement.style.fill = 'transparent';
pathElement.style.cursor = 'pointer';
// Add click event to show hex ID
pathElement.addEventListener('click', (e) => {
e.stopPropagation();
this.showHexId(hex.id, center.x, center.y);
});
hexGroup.appendChild(pathElement);
this.svg.appendChild(hexGroup);
// Add hex number (if not desert)
if (hex.number !== null) {
const numberElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
numberElement.setAttribute('x', center.x);
numberElement.setAttribute('y', center.y);
numberElement.textContent = hex.number;
numberElement.setAttribute('class', hex.number === 6 || hex.number === 8 ? 'hex-number red' : 'hex-number');
this.svg.appendChild(numberElement);
}
// Add robber if present
if (hex.has_robber || hex.robber) { // Support both formats for backward compatibility
this.createRobber(center.x, center.y, hex.id);
}
}
createRobber(x, y, hexId) {
// Create robber group
const robberGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
robberGroup.setAttribute('class', 'robber');
robberGroup.setAttribute('data-hex-id', hexId);
// Create robber circle
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', 18);
// Create robber text
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', x);
text.setAttribute('y', y);
text.textContent = 'R';
text.setAttribute('class', 'robber-text');
robberGroup.appendChild(circle);
robberGroup.appendChild(text);
this.svg.appendChild(robberGroup);
}
createVertices() {
// Create vertices group
const verticesGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
verticesGroup.setAttribute('id', 'vertices');
if (this.showVertices) {
verticesGroup.classList.add('vertices-visible');
}
this.vertices.forEach(vertex => {
// Create vertex circle
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', vertex.x);
circle.setAttribute('cy', vertex.y);
circle.setAttribute('r', 8);
circle.setAttribute('class', 'vertex');
circle.setAttribute('data-vertex-id', vertex.id);
// Create vertex number text
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', vertex.x);
text.setAttribute('y', vertex.y);
text.textContent = vertex.id;
text.setAttribute('class', 'vertex-number');
verticesGroup.appendChild(circle);
verticesGroup.appendChild(text);
});
this.svg.appendChild(verticesGroup);
}
createSettlement(settlement) {
// Find vertex by ID (handle both string and number IDs)
const vertexId = parseInt(settlement.vertex);
const vertex = this.vertices.find(v => v.id === vertexId);
if (!vertex) {
console.warn(`Could not find vertex ${settlement.vertex} for settlement`);
return;
}
// Create settlement polygon (house shape)
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
const points = [
[vertex.x, vertex.y - 12], // top
[vertex.x - 8, vertex.y - 4], // top-left
[vertex.x - 8, vertex.y + 8], // bottom-left
[vertex.x + 8, vertex.y + 8], // bottom-right
[vertex.x + 8, vertex.y - 4] // top-right
].map(p => p.join(',')).join(' ');
polygon.setAttribute('points', points);
polygon.setAttribute('class', `settlement player${settlement.player}`);
polygon.setAttribute('data-settlement-id', settlement.id);
polygon.setAttribute('data-vertex-id', settlement.vertex);
this.svg.appendChild(polygon);
}
createCity(city) {
// Find vertex by ID (handle both string and number IDs)
const vertexId = parseInt(city.vertex);
const vertex = this.vertices.find(v => v.id === vertexId);
if (!vertex) {
console.warn(`Could not find vertex ${city.vertex} for city`);
return;
}
// Create city polygon (larger building)
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
const points = [
[vertex.x, vertex.y - 16], // top
[vertex.x - 12, vertex.y - 8], // top-left
[vertex.x - 12, vertex.y + 12], // bottom-left
[vertex.x + 12, vertex.y + 12], // bottom-right
[vertex.x + 12, vertex.y - 8] // top-right
].map(p => p.join(',')).join(' ');
polygon.setAttribute('points', points);
polygon.setAttribute('class', `city player${city.player}`);
polygon.setAttribute('data-city-id', city.id);
polygon.setAttribute('data-vertex-id', city.vertex);
this.svg.appendChild(polygon);
}
createRoad(road) {
// Find vertices by ID (handle both string and number IDs)
const fromId = parseInt(road.from);
const toId = parseInt(road.to);
const fromVertex = this.vertices.find(v => v.id === fromId);
const toVertex = this.vertices.find(v => v.id === toId);
if (!fromVertex || !toVertex) {
console.warn(`Could not find vertices ${road.from}->${road.to} for road`);
return;
}
// Create road line
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', fromVertex.x);
line.setAttribute('y1', fromVertex.y);
line.setAttribute('x2', toVertex.x);
line.setAttribute('y2', toVertex.y);
line.setAttribute('class', `road player${road.player}`);
line.setAttribute('data-road-id', road.id);
line.setAttribute('data-from-vertex', road.from);
line.setAttribute('data-to-vertex', road.to);
this.svg.appendChild(line);
}
createHarbor(harbor) {
// Harbor is positioned on an edge between two points
// We'll place it at the midpoint between the two vertices
// Harbor types: 'wood', 'sheep', 'brick', 'wheat', 'ore', 'any'
// Harbor ratios: 2 (for specific resource) or 3 (for any resource)
const harborTypeColors = {
'wood': '#228B22', // Forest Green
'sheep': '#90EE90', // Light Green
'brick': '#CD5C5C', // Indian Red
'wheat': '#FFD700', // Gold
'ore': '#696969', // Dim Gray
'any': '#4169E1' // Royal Blue
};
const harborTypeIcons = {
'wood': '๐ŸŒฒ',
'sheep': '๐Ÿ‘',
'brick': '๐Ÿงฑ',
'wheat': '๐ŸŒพ',
'ore': 'โ›ฐ๏ธ',
'any': '๐Ÿช'
};
// Find the two vertices for this harbor
const pointOne = this.getVertexByPointId(harbor.point_one);
const pointTwo = this.getVertexByPointId(harbor.point_two);
if (!pointOne || !pointTwo) {
console.warn(`Could not find vertices for harbor ${harbor.id}: points ${harbor.point_one} and ${harbor.point_two}`);
return;
}
// Calculate midpoint between the two vertices
const midX = (pointOne.x + pointTwo.x) / 2;
const midY = (pointOne.y + pointTwo.y) / 2;
// Calculate direction vector to push harbor outward from the board
const centerX = this.centerX;
const centerY = this.centerY;
const dx = midX - centerX;
const dy = midY - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
// Push harbor 45 pixels outward from the center
const offsetDist = 45;
const x = midX + (dx / dist) * offsetDist;
const y = midY + (dy / dist) * offsetDist;
// Create harbor group
const harborGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
harborGroup.setAttribute('class', 'harbor');
harborGroup.setAttribute('data-harbor-id', harbor.id);
harborGroup.setAttribute('data-harbor-type', harbor.type);
harborGroup.setAttribute('data-point-one', harbor.point_one);
harborGroup.setAttribute('data-point-two', harbor.point_two);
// Create harbor circle background
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', 16);
circle.setAttribute('fill', harborTypeColors[harbor.type] || '#4169E1');
circle.setAttribute('stroke', 'white');
circle.setAttribute('stroke-width', 2.5);
circle.setAttribute('opacity', '0.95');
// Create harbor icon/text
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', x);
text.setAttribute('y', y + 5);
text.setAttribute('text-anchor', 'middle');
text.setAttribute('fill', 'white');
text.setAttribute('font-size', '14');
text.textContent = harborTypeIcons[harbor.type] || '๐Ÿช';
// Create harbor ratio text (smaller, below the circle)
const ratioText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
ratioText.setAttribute('x', x);
ratioText.setAttribute('y', y + 26);
ratioText.setAttribute('text-anchor', 'middle');
ratioText.setAttribute('fill', 'white');
ratioText.setAttribute('font-size', '8');
ratioText.setAttribute('font-weight', 'bold');
ratioText.textContent = `${harbor.ratio}:1`;
// Create lines connecting harbor to both vertices
const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line1.setAttribute('x1', pointOne.x);
line1.setAttribute('y1', pointOne.y);
line1.setAttribute('x2', x);
line1.setAttribute('y2', y);
line1.setAttribute('stroke', harborTypeColors[harbor.type] || '#4169E1');
line1.setAttribute('stroke-width', 2.5);
line1.setAttribute('opacity', '0.7');
const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line2.setAttribute('x1', pointTwo.x);
line2.setAttribute('y1', pointTwo.y);
line2.setAttribute('x2', x);
line2.setAttribute('y2', y);
line2.setAttribute('stroke', harborTypeColors[harbor.type] || '#4169E1');
line2.setAttribute('stroke-width', 2.5);
line2.setAttribute('opacity', '0.7');
// Add tooltip
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
title.textContent = `Harbor: ${harbor.type} (${harbor.ratio}:1 trade)\nPoints: ${harbor.point_one}-${harbor.point_two}`;
harborGroup.appendChild(title);
// Add lines first (so they appear behind the circle)
harborGroup.appendChild(line1);
harborGroup.appendChild(line2);
harborGroup.appendChild(circle);
harborGroup.appendChild(text);
harborGroup.appendChild(ratioText);
this.svg.appendChild(harborGroup);
console.log(`๐Ÿช Created harbor: ${harbor.type} (${harbor.ratio}:1) at points ${harbor.point_one}-${harbor.point_two}`);
}
// Update board from game state (called when receiving updates from server)
updateFromGameState(gameState) {
console.log('Updating board from game state:', gameState);
// Store the current game state
this.currentGameState = gameState;
// Don't regenerate vertices - they're already loaded from server in init()
// this.generateVertices(); // <- This function doesn't exist!
// Clear and rebuild board with new data (but not buildings)
this.svg.innerHTML = '';
// Create hexes from game state
if (gameState.hexes && gameState.hexes.length > 0) {
console.log(`Creating ${gameState.hexes.length} hexes from server data`);
gameState.hexes.forEach(hex => {
this.createHex(hex);
});
}
// Create vertices
this.createVertices();
// Add harbors from game state (before buildings so they appear behind)
if (gameState.harbors && gameState.harbors.length > 0) {
console.log(`Creating ${gameState.harbors.length} harbors from server data`);
gameState.harbors.forEach(harbor => {
this.createHarbor(harbor);
});
}
// Add buildings from server data
this.updateBuildings(gameState);
this.updateRobberFromGameState(gameState);
// Update transform
this.updateTransform();
}
updateBuildings(gameState) {
// Remove existing buildings
const existingBuildings = this.svg.querySelectorAll('.settlement, .city, .road');
existingBuildings.forEach(building => building.remove());
// Add settlements from server data
if (gameState.settlements && gameState.settlements.length > 0) {
console.log('Adding settlements:', gameState.settlements);
gameState.settlements.forEach(settlement => {
this.createSettlement(settlement);
});
}
// Add cities from server data
if (gameState.cities && gameState.cities.length > 0) {
console.log('Adding cities:', gameState.cities);
gameState.cities.forEach(city => {
this.createCity(city);
});
}
// Add roads from server data
if (gameState.roads && gameState.roads.length > 0) {
console.log('Adding roads:', gameState.roads);
gameState.roads.forEach(road => {
this.createRoad(road);
});
}
}
updateRobberFromGameState(gameState) {
// Remove existing robber
const existingRobber = this.svg.querySelector('.robber');
if (existingRobber) {
existingRobber.remove();
}
// Add robber from server data
if (gameState.hexes) {
// Find hex with has_robber set to true
const robberHex = gameState.hexes.find(h => h.has_robber === true);
if (robberHex) {
// Use axial coordinates from the hex data
const q = robberHex.axial_coords ? robberHex.axial_coords[0] : robberHex.q;
const r = robberHex.axial_coords ? robberHex.axial_coords[1] : robberHex.r;
const center = this.hexToPixel(q, r);
this.createRobber(center.x, center.y, robberHex.id);
console.log('๐Ÿดโ€โ˜ ๏ธ Robber placed at hex ID:', robberHex.id, 'position:', robberHex.position);
} else {
console.log('No robber found in game state');
}
}
}
updateRobberPosition(newPosition) {
// Remove existing robber
const existingRobber = this.svg.querySelector('.robber');
if (existingRobber) {
existingRobber.remove();
}
// Add robber to new position
const hex = GAMEDATA.hexes.find(h => h.id === newPosition);
if (hex) {
const center = this.hexToPixel(hex.q, hex.r);
this.createRobber(center.x, center.y, hex.id);
}
}
// Zoom and pan functionality
handleZoom(e) {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
this.zoomLevel = Math.max(0.5, Math.min(3, this.zoomLevel * delta));
this.updateTransform();
}
startDrag(e) {
this.isDragging = true;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
}
handleDrag(e) {
if (!this.isDragging) return;
const deltaX = e.clientX - this.lastMouseX;
const deltaY = e.clientY - this.lastMouseY;
this.panX += deltaX;
this.panY += deltaY;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
this.updateTransform();
}
endDrag() {
this.isDragging = false;
}
updateTransform() {
this.svg.style.transform = `translate(${this.panX}px, ${this.panY}px) scale(${this.zoomLevel})`;
}
// Control functions
zoomIn() {
this.zoomLevel = Math.min(3, this.zoomLevel * 1.2);
this.updateTransform();
}
zoomOut() {
this.zoomLevel = Math.max(0.5, this.zoomLevel * 0.8);
this.updateTransform();
}
resetZoom() {
this.zoomLevel = 1;
this.panX = 0;
this.panY = 0;
this.updateTransform();
}
toggleVertices() {
this.showVertices = !this.showVertices;
const verticesGroup = this.svg.querySelector('#vertices');
if (verticesGroup) {
if (this.showVertices) {
verticesGroup.classList.add('vertices-visible');
} else {
verticesGroup.classList.remove('vertices-visible');
}
}
// Update button text
const button = document.getElementById('toggleVertices');
if (button) {
button.textContent = this.showVertices ? '๐Ÿ”' : '๐Ÿ“';
}
}
// ืขื“ื›ื•ืŸ vertex IDs ื‘ื”ืชื‘ืกืก ืขืœ ืžื™ืคื•ื™ ืืžื™ืชื™ ืžื”ืฉืจืช
updateVertexIDsFromMapping() {
if (!window.pointMapping || !this.vertices) {
console.warn('โš ๏ธ ืœื ื ื™ืชืŸ ืœืขื“ื›ืŸ vertex IDs - ื—ืกืจ ืžื™ืคื•ื™ ืื• vertices');
return;
}
console.log('๐Ÿ”„ ืžืขื“ื›ืŸ vertex IDs ืœืคื™ ื”ืžื™ืคื•ื™ ื”ืืžื™ืชื™...');
// ืขื‘ื•ืจ ืขืœ ื›ืœ vertex ื•ื‘ื“ื•ืง ืื ื™ืฉ ืœื• ืžื™ืคื•ื™ ืžืชืื™ื
this.vertices.forEach((vertex, index) => {
// ื ืกื” ืœืžืฆื•ื ื”ืชืืžื” ื‘ืžื™ืคื•ื™ ืœืคื™ ืžื™ืงื•ื ื™ื—ืกื™ ืื• ืื™ื ื“ืงืก
// ื–ื”ื• approx - ื‘ืžืฆื™ืื•ืช ืฆืจื™ืš ืžื™ืคื•ื™ ืžื“ื•ื™ืง ื™ื•ืชืจ
// ืžืฉืชืžืฉ ื‘ืื™ื ื“ืงืก ื›ืชื•ืฆืืช ื”ื“ืžื•ื™
const mappedPointId = index + 1;
// ืขื“ื›ื•ืŸ ื”-ID ืฉืœ ื”vertex
vertex.originalId = vertex.id; // ืฉื•ืžืจ ืืช ื”ID ื”ืžืงื•ืจื™
vertex.id = mappedPointId; // ืžืขื“ื›ืŸ ืœID ื”ื ื›ื•ืŸ
});
// ืขื“ื›ื•ืŸ ื”ืชืฆื•ื’ื” ืื ื”vertices ืžื•ืฆื’ื™ื
this.refreshVertexDisplay();
console.log('โœ… vertex IDs ืขื•ื“ื›ื ื• ื‘ื”ืชื‘ืกืก ืขืœ ื”ืžื™ืคื•ื™');
}
// ืจืขื ื•ืŸ ืชืฆื•ื’ืช vertices ืขื IDs ืžืขื•ื“ื›ื ื™ื
refreshVertexDisplay() {
const verticesGroup = this.svg.querySelector('#vertices');
if (!verticesGroup) return;
// ืขื“ื›ื•ืŸ ื”ื˜ืงืกื˜ื™ื ืขื ื”ืžืกืคืจื™ื ื”ื—ื“ืฉื™ื
const vertexTexts = verticesGroup.querySelectorAll('.vertex-number');
vertexTexts.forEach((text, index) => {
if (this.vertices[index]) {
text.textContent = this.vertices[index].id;
}
});
// ืขื“ื›ื•ืŸ ื”-data attributes
const vertexCircles = verticesGroup.querySelectorAll('.vertex');
vertexCircles.forEach((circle, index) => {
if (this.vertices[index]) {
circle.setAttribute('data-vertex-id', this.vertices[index].id);
}
});
}
// Debug functions
logAllVertices() {
console.log('All vertices:', this.vertices);
}
logVertexConnections() {
// Show examples of connected vertices
const examples = this.vertices.slice(0, 5);
examples.forEach(vertex => {
const connected = this.getConnectedVertices(vertex.id);
console.log(`Vertex ${vertex.id} connects to vertices: ${connected.join(', ')}`);
});
}
getConnectedVertices(vertexId) {
// This would need implementation based on hex grid logic
// For now, return empty array
return [];
}
// Show hex ID when clicked
showHexId(hexId, centerX, centerY) {
// Remove any existing hex ID display
const existingDisplay = this.svg.querySelector('.hex-id-display');
if (existingDisplay) {
existingDisplay.remove();
}
// Create a group for the display
const displayGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
displayGroup.setAttribute('class', 'hex-id-display');
// Create background rectangle
const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
bg.setAttribute('x', centerX - 40);
bg.setAttribute('y', centerY - 25);
bg.setAttribute('width', 80);
bg.setAttribute('height', 50);
bg.setAttribute('rx', 5);
bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
bg.setAttribute('stroke', '#FFD700');
bg.setAttribute('stroke-width', 2);
// Create text for "Tile ID:"
const labelText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
labelText.setAttribute('x', centerX);
labelText.setAttribute('y', centerY - 5);
labelText.setAttribute('text-anchor', 'middle');
labelText.setAttribute('fill', '#FFD700');
labelText.setAttribute('font-size', '12');
labelText.setAttribute('font-weight', 'bold');
labelText.textContent = 'Tile ID:';
// Create text for hex ID
const idText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
idText.setAttribute('x', centerX);
idText.setAttribute('y', centerY + 15);
idText.setAttribute('text-anchor', 'middle');
idText.setAttribute('fill', 'white');
idText.setAttribute('font-size', '20');
idText.setAttribute('font-weight', 'bold');
idText.textContent = hexId;
displayGroup.appendChild(bg);
displayGroup.appendChild(labelText);
displayGroup.appendChild(idText);
this.svg.appendChild(displayGroup);
// Auto-remove after 3 seconds
setTimeout(() => {
if (displayGroup.parentNode) {
displayGroup.remove();
}
}, 3000);
// Log to console as well
console.log(`๐ŸŽฏ Clicked on Tile ID: ${hexId}`);
}
}