126 lines
3.6 KiB
JavaScript
126 lines
3.6 KiB
JavaScript
// Hex-grid maths shared by the server simulation and the browser view.
|
|
//
|
|
// The map is a flat-top hexagonal grid stored in "offset" coordinates
|
|
// (x = column, y = row) with odd columns pushed down by half a hex, matching
|
|
// the source art layout (tile_size 63x56, vertical offset axis). Everything
|
|
// that walks the grid -- the generator, pathfinding, territory, fog and the
|
|
// renderer -- goes through this module so they all agree on neighbourhoods.
|
|
|
|
export const HEX_W = 63;
|
|
export const HEX_H = 56;
|
|
|
|
// Horizontal distance between adjacent columns of a flat-top hex grid.
|
|
export const COL_STEP = HEX_W * 0.75;
|
|
|
|
// Cube directions for a flat-top axial grid.
|
|
const CUBE_DIRECTIONS = [
|
|
[1, 0],
|
|
[1, -1],
|
|
[0, -1],
|
|
[-1, 0],
|
|
[-1, 1],
|
|
[0, 1],
|
|
];
|
|
|
|
export function key(x, y) {
|
|
return x + "," + y;
|
|
}
|
|
|
|
export function parseKey(k) {
|
|
const i = k.indexOf(",");
|
|
return { x: Number(k.slice(0, i)), y: Number(k.slice(i + 1)) };
|
|
}
|
|
|
|
// Parity of a (possibly negative) integer: 1 for odd, 0 for even.
|
|
export function parity(x) {
|
|
return x & 1;
|
|
}
|
|
|
|
export function offsetToCube(x, y) {
|
|
const q = x;
|
|
const r = y - Math.floor((x - parity(x)) / 2);
|
|
return { x: q, y: r, z: -q - r };
|
|
}
|
|
|
|
export function cubeToOffset(q, r) {
|
|
return { x: q, y: r + Math.floor((q - parity(q)) / 2) };
|
|
}
|
|
|
|
export function cubeDistance(a, b) {
|
|
return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y), Math.abs(a.z - b.z));
|
|
}
|
|
|
|
// Pixel centre of a tile, in world space, with (0,0) at the origin.
|
|
export function mapToLocal(x, y) {
|
|
return { x: COL_STEP * x, y: HEX_H * (y + 0.5 * parity(x)) };
|
|
}
|
|
|
|
export class MapTopology {
|
|
constructor(size, cylindrical) {
|
|
this.width = size.x;
|
|
this.height = size.y;
|
|
this.originX = -Math.floor(size.x / 2);
|
|
this.originY = -Math.floor(size.y / 2);
|
|
this.cylindrical = !!cylindrical;
|
|
}
|
|
|
|
isCylindrical() {
|
|
return this.cylindrical;
|
|
}
|
|
|
|
wrapX(value) {
|
|
if (!this.cylindrical || this.width <= 0) return value;
|
|
const rel = ((value - this.originX) % this.width + this.width) % this.width;
|
|
return rel + this.originX;
|
|
}
|
|
|
|
wrapCoords(x, y) {
|
|
return { x: this.wrapX(x), y };
|
|
}
|
|
|
|
// Shortest signed offset from `from` to `to`, folding the wrapped axis.
|
|
wrappedDelta(from, to) {
|
|
let dx = to.x - from.x;
|
|
const dy = to.y - from.y;
|
|
if (this.cylindrical && this.width > 0) {
|
|
dx = ((dx + this.width / 2) % this.width + this.width) % this.width - this.width / 2;
|
|
}
|
|
return { x: dx, y: dy };
|
|
}
|
|
|
|
// Hex distance using the shortest wrapped offset.
|
|
tileDistance(from, to) {
|
|
const delta = this.wrappedDelta(from, to);
|
|
const a = offsetToCube(from.x, from.y);
|
|
const b = offsetToCube(from.x + delta.x, from.y + delta.y);
|
|
return cubeDistance(a, b);
|
|
}
|
|
|
|
// The six neighbours of a cell, folded back into the map when wrapping.
|
|
neighbours(x, y) {
|
|
const cube = offsetToCube(x, y);
|
|
const result = [];
|
|
for (const [dq, dr] of CUBE_DIRECTIONS) {
|
|
const offset = cubeToOffset(cube.x + dq, cube.y + dr);
|
|
const wrapped = this.wrapCoords(offset.x, offset.y);
|
|
result.push(wrapped);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Pixel vector from one tile to the nearest copy of another, following the
|
|
// shortest wrapped route.
|
|
pixelDelta(from, to) {
|
|
const delta = this.wrappedDelta(from, to);
|
|
const a = mapToLocal(from.x, from.y);
|
|
const b = mapToLocal(from.x + delta.x, from.y + delta.y);
|
|
return { x: b.x - a.x, y: b.y - a.y };
|
|
}
|
|
|
|
// Horizontal pixel period: distance between a tile and its wrapped copy.
|
|
periodPixels() {
|
|
if (!this.cylindrical || this.width <= 0) return 0;
|
|
return COL_STEP * this.width;
|
|
}
|
|
}
|