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 = {
@@ -96,12 +95,13 @@ class GameUI {
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.offsetX = (canvasWidth - mapWidth) / 2 + HEX_SIZE * sqrt3;
this.offsetY = HEX_SIZE * 2; 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);
return { // Convert axial to pixel coordinates for pointy-top hex
x: this.offsetX + isoX, const x = this.offsetX + HEX_SIZE * sqrt3 * (q + r/2);
y: this.offsetY + isoY const y = this.offsetY + HEX_SIZE * 1.5 * r;
};
return { x, y };
} }
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 = [];
@@ -186,19 +182,18 @@ 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;
// Hexagon vertices for isometric view (elongated horizontally) // Pointy-top hexagon vertices (flat sides left/right)
const vertices = [ const vertices = [];
{ x: x + hexWidth, y: y }, // right for (let i = 0; i < 6; i++) {
{ x: x + hexWidth * 0.5, y: y - hexHeight }, // top-right const angle = Math.PI / 6 + (Math.PI / 3) * i; // Start at 30 degrees
{ x: x - hexWidth * 0.5, y: y - hexHeight }, // top-left vertices.push({
{ x: x - hexWidth, y: y }, // left x: x + size * Math.cos(angle),
{ x: x - hexWidth * 0.5, y: y + hexHeight }, // bottom-left y: y + size * Math.sin(angle)
{ 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);
@@ -247,17 +242,16 @@ 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 = [];
{ x: x + hexWidth, y: y }, for (let i = 0; i < 6; i++) {
{ x: x + hexWidth * 0.5, y: y - hexHeight }, const angle = Math.PI / 6 + (Math.PI / 3) * i;
{ x: x - hexWidth * 0.5, y: y - hexHeight }, vertices.push({
{ x: x - hexWidth, y: y }, x: x + size * Math.cos(angle),
{ x: x - hexWidth * 0.5, y: y + hexHeight }, y: y + size * Math.sin(angle)
{ 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);