Fix hex grid rendering - proper pointy-top hexagons with tight adjacency

This commit is contained in:
sokol
2026-02-21 17:36:19 +03:00
parent 25385de4d4
commit 139a732378

View File

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