From 139a732378db1ea49d9bd9ac4fe89a2f98907adb Mon Sep 17 00:00:00 2001 From: sokol Date: Sat, 21 Feb 2026 17:36:19 +0300 Subject: [PATCH] Fix hex grid rendering - proper pointy-top hexagons with tight adjacency --- public/game.js | 132 +++++++++++++++++++++++-------------------------- 1 file changed, 63 insertions(+), 69 deletions(-) diff --git a/public/game.js b/public/game.js index 6b502cf..7d0c863 100644 --- a/public/game.js +++ b/public/game.js @@ -5,10 +5,9 @@ import { HexMap, CELL_TYPES } from './map.js'; // Game constants -const HEX_SIZE = 20; +const HEX_SIZE = 18; const MAP_SIZE = 20; const ANIMATION_DURATION = 300; -const ISO_ANGLE = Math.PI / 6; // 30 degrees for isometric view // Colors const COLORS = { @@ -95,13 +94,14 @@ class GameUI { centerMap() { const canvasWidth = this.canvas.width; const canvasHeight = this.canvas.height; - - // Calculate map dimensions for isometric view - const mapWidth = MAP_SIZE * HEX_SIZE * 2; - const mapHeight = MAP_SIZE * HEX_SIZE; - - this.offsetX = canvasWidth / 2; - this.offsetY = HEX_SIZE * 2; + + const sqrt3 = Math.sqrt(3); + // Map dimensions for pointy-top hex grid + const mapWidth = HEX_SIZE * sqrt3 * (MAP_SIZE + MAP_SIZE/2); + const mapHeight = HEX_SIZE * 1.5 * (MAP_SIZE - 1) + HEX_SIZE * 2; + + this.offsetX = (canvasWidth - mapWidth) / 2 + HEX_SIZE * sqrt3; + this.offsetY = (canvasHeight - mapHeight) / 2 + HEX_SIZE; } setupEventListeners() { @@ -115,27 +115,26 @@ class GameUI { } hexToPixel(q, r) { - // Isometric projection for pointy-top hex grid - // q axis goes top-left to bottom-right - // r axis goes top-right to bottom-left - const isoX = (q - r) * HEX_SIZE * 1.5; - const isoY = (q + r) * HEX_SIZE * 0.75; + // Pointy-top hex grid with proper adjacency + // For pointy-top: width = sqrt(3) * size, height = 2 * size + // Horizontal spacing = width = sqrt(3) * size + // Vertical spacing = 3/4 * height = 1.5 * size + const sqrt3 = Math.sqrt(3); + + // Convert axial to pixel coordinates for pointy-top hex + const x = this.offsetX + HEX_SIZE * sqrt3 * (q + r/2); + const y = this.offsetY + HEX_SIZE * 1.5 * r; - return { - x: this.offsetX + isoX, - y: this.offsetY + isoY - }; + return { x, y }; } pixelToHex(x, y) { const adjX = x - this.offsetX; const adjY = y - this.offsetY; - // Reverse isometric projection - // From: isoX = (q - r) * HEX_SIZE * 1.5 - // isoY = (q + r) * HEX_SIZE * 0.75 - const q = (adjX / (HEX_SIZE * 1.5) + adjY / (HEX_SIZE * 0.75)) / 2; - const r = (adjY / (HEX_SIZE * 0.75) - adjX / (HEX_SIZE * 1.5)) / 2; + const sqrt3 = Math.sqrt(3); + const q = (adjX / (HEX_SIZE * sqrt3) - adjY / (HEX_SIZE * 1.5) / 2); + const r = adjY / (HEX_SIZE * 1.5); const qi = Math.round(q); const ri = Math.round(r); @@ -147,26 +146,23 @@ class GameUI { } /** - * Get neighboring cells for isometric hex grid - * Directions for pointy-top hex in axial coordinates with isometric projection: - * hexToPixel: isoX = (q-r)*1.5, isoY = (q+r)*0.75 - * - * Visual directions: - * - top: (-1, -1) - both decrease - * - bottom: (+1, +1) - both increase - * - top-left: (-1, 0) - q decreases - * - bottom-left: (0, +1) - r increases - * - top-right: (0, -1) - r decreases - * - bottom-right: (+1, 0) - q increases + * Get neighboring cells for pointy-top hex grid + * Directions for pointy-top hex in axial coordinates: + * - north-east: (+1, -1) + * - north-west: (0, -1) + * - west: (-1, 0) + * - south-west: (-1, +1) + * - south-east: (0, +1) + * - east: (+1, 0) */ getValidTargets(q, r) { const directions = [ - [+1, 0], // bottom-right - [+1, +1], // bottom (straight down) - [0, +1], // bottom-left - [-1, 0], // top-left - [-1, -1], // top (straight up) - [0, -1] // top-right + [+1, -1], // north-east + [0, -1], // north-west + [-1, 0], // west + [-1, +1], // south-west + [0, +1], // south-east + [+1, 0] // east ]; const targets = []; @@ -185,31 +181,30 @@ class GameUI { drawHex(q, r, fillStyle, strokeStyle = COLORS.stroke, lineWidth = 2) { const { x, y } = this.hexToPixel(q, r); - - // Draw elongated hexagon for isometric view - const hexWidth = HEX_SIZE * 1.3; - const hexHeight = HEX_SIZE * 0.75; - - // Hexagon vertices for isometric view (elongated horizontally) - const vertices = [ - { x: x + hexWidth, y: y }, // right - { x: x + hexWidth * 0.5, y: y - hexHeight }, // top-right - { x: x - hexWidth * 0.5, y: y - hexHeight }, // top-left - { x: x - hexWidth, y: y }, // left - { x: x - hexWidth * 0.5, y: y + hexHeight }, // bottom-left - { x: x + hexWidth * 0.5, y: y + hexHeight } // bottom-right - ]; - + + // Draw pointy-top hexagon - size matches grid spacing + const size = HEX_SIZE * 0.98; // Almost full size for tight fit + + // Pointy-top hexagon vertices (flat sides left/right) + const vertices = []; + for (let i = 0; i < 6; i++) { + const angle = Math.PI / 6 + (Math.PI / 3) * i; // Start at 30 degrees + vertices.push({ + x: x + size * Math.cos(angle), + y: y + size * Math.sin(angle) + }); + } + this.ctx.beginPath(); this.ctx.moveTo(vertices[0].x, vertices[0].y); for (let i = 1; i < vertices.length; i++) { this.ctx.lineTo(vertices[i].x, vertices[i].y); } this.ctx.closePath(); - + this.ctx.fillStyle = fillStyle; this.ctx.fill(); - + this.ctx.strokeStyle = strokeStyle; this.ctx.lineWidth = lineWidth; this.ctx.stroke(); @@ -247,18 +242,17 @@ class GameUI { const ownerColor = cell.getOwner() === 1 ? COLORS.player1 : COLORS.player2; // Draw border matching the hexagon shape - const hexWidth = HEX_SIZE * 1.3 - 3; - const hexHeight = HEX_SIZE * 0.75 - 3; - - const vertices = [ - { x: x + hexWidth, y: y }, - { x: x + hexWidth * 0.5, y: y - hexHeight }, - { x: x - hexWidth * 0.5, y: y - hexHeight }, - { x: x - hexWidth, y: y }, - { x: x - hexWidth * 0.5, y: y + hexHeight }, - { x: x + hexWidth * 0.5, y: y + hexHeight } - ]; - + const size = HEX_SIZE * 0.98 - 3; + + const vertices = []; + for (let i = 0; i < 6; i++) { + const angle = Math.PI / 6 + (Math.PI / 3) * i; + vertices.push({ + x: x + size * Math.cos(angle), + y: y + size * Math.sin(angle) + }); + } + this.ctx.beginPath(); this.ctx.moveTo(vertices[0].x, vertices[0].y); for (let i = 1; i < vertices.length; i++) {