import { ITopology } from './Topology'; export type StaggeredIsoEdgeDirection = 'ne' | 'se' | 'sw' | 'nw'; export type StaggeredIsoDirection = StaggeredIsoEdgeDirection | 'n' | 'e' | 's' | 'w'; type Offset = readonly [number, number]; /** * A staggered isometric grid, the way Civ2 and Freeciv store one: diamond * tiles in rows, every odd row shifted half a tile right, so the world is * still a rectangle on screen and still wraps east to west. * * Storage is `width × height` as for the square grid, but a tile's storage * neighbours depend on its row: * * ``` * e (x + 1, y) w (x - 1, y) share a corner * n (x, y - 2) s (x, y + 2) share a corner * ne (x + (y & 1), y - 1) nw (x - 1 + (y & 1), y - 1) share an edge * se (x + (y & 1), y + 1) sw (x - 1 + (y & 1), y + 1) share an edge * ``` * * Directions are named as they look on screen. The four that share an edge * are the diagonals, `ne se sw nw`, which is why anything that wants the tiles * sharing an edge must ask for `edgeDirections()` rather than assume `n e s w`. * * Two coordinate frames make the rest simple: * * - `d = 2x + (y & 1)` is the tile's column in half-tile steps, so (`d`, `y`) * is where its centre is drawn. * - `a = (d + y) / 2` and `b = (y - d) / 2` are its *logical* coordinates. In * (`a`, `b`) this is exactly the square grid turned through 45°: `se` is * `+a`, `sw` is `+b`, `e` is `+a -b`, `s` is `+a +b`, and so on. So * neighbours, areas and distances mean what they mean on the square grid, * and every rule written for it carries over unchanged. * * The world always wraps east to west. It wraps north to south only when its * height is even (the default when `wrapY` is not given), because a seam * between two rows of the same parity would leave the stagger out of step; * `validate` refuses an odd height that is asked to wrap. Off a top or bottom * edge that does not wrap there is nothing: `neighbour` returns `null` and * `area` leaves those tiles out. */ export declare class StaggeredIsoTopology implements ITopology { private _height; private _width; private _wrapY; constructor(width: number, height: number, wrapY?: boolean); /** * The Chebyshev box of side `2 * radius + 1` in logical coordinates centred * on (`x`, `y`) — the square grid's box, so 9 tiles at radius 1 and 25 at * radius 2 — on screen a diamond of diamonds. * * Ordered `a`-major like `SquareTopology.area`, so the same logical tiles * come in the same order: `a` outer, `b` inner, each from `-radius` to * `+radius`. On screen that is one line of the box at a time, the lines * taken from its north-west side to its south-east side and each walked * from its north-east end to its south-west end. At radius 1 that is * `n nw w ne (centre) sw e se s`. * * Coordinates are wrapped into range as `neighbour` wraps them. Rows off a * top or bottom edge that does not wrap are left out, so an area near a * pole has fewer tiles. A world narrower or shorter than the box repeats * tiles rather than dropping them. */ area(x: number, y: number, radius: number): [number, number][]; /** `n ne e se s sw w nw`, as they look on screen. */ directions(): readonly StaggeredIsoDirection[]; /** * Straight-line distance in logical coordinates, `hypot(Δa, Δb)`, measured * the short way round: the same measure, and for tiles away from the seams * the same number, as `SquareTopology.distance` over the logical tiles. * * Wrapping is less tidy than on the square grid in (`a`, `b`): going `width` * columns round the world moves `a` by `+width` and `b` by `-width` * together, so the shortest horizontal and the shortest vertical cannot be * picked one axis at a time there. They can in (`d`, `y`) — the centres' * half-tile column and the row. `Δa² + Δb²` is `(Δd² + Δy²) / 2`, wrapping * east to west moves only `d` (by `2 * width`), and wrapping north to south * (an even height, so parity is kept) moves only `y`. So the shortest of * the three horizontal candidates times the three vertical ones is the * shortest `Δd` paired with the shortest `Δy`, then turned back into * (`Δa`, `Δb`) for the `hypot` — as magnitudes, so possibly swapped and with * their signs lost, neither of which `hypot` sees. Nothing allocated, as on * the square grid. * * `Math.hypot` of the logical deltas, not `Math.sqrt` of the half-tile * ones, so that the number is bit-identical to the square grid's for the * same logical tiles. */ distance(x1: number, y1: number, x2: number, y2: number): number; /** `ne se sw nw`: the diagonals are the directions that share an edge. */ edgeDirections(): readonly StaggeredIsoEdgeDirection[]; /** `'staggered-iso'`. */ id(): 'staggered-iso'; /** * Wraps east to west always, and north to south when `wrapsY()`; `null` off * a top or bottom edge that does not. */ neighbour(x: number, y: number, direction: StaggeredIsoDirection): [number, number] | null; /** * A `switch` over shared, frozen tuples, as `SquareTopology.offset`; the * edge directions pick between the even-row and the odd-row step. `x` is * unused. */ offset(direction: StaggeredIsoDirection, x: number, y: number): Offset; /** * King's moves in logical coordinates, `max(|Δa|, |Δb|)`, the short way * round: every one of the eight neighbours is one square-grid step in * (`a`, `b`), so this is `SquareTopology.steps` over the logical tiles. * * The shortest way round is picked in (`d`, `y`), for the reason `distance` * gives. There `max(|Δa|, |Δb|)` is `max(|Δd + Δy|, |Δy - Δd|) / 2`, which is * `(|Δd| + |Δy|) / 2`: a sum, which grows with both, so the shortest `Δd` * and the shortest `Δy` give the fewest steps. `Δd` and `Δy` always have the * same parity (a wrap moves `d` by `2 * width` and `y` by an even height), * so the halving is exact. Nothing is allocated. */ steps(x1: number, y1: number, x2: number, y2: number): number; /** * Any whole width and height of at least one, and an even height if the * world is to wrap north to south. */ validate(width: number, height: number): void; wrapsX(): boolean; wrapsY(): boolean; } export default StaggeredIsoTopology;