Server-authoritative Godot 4.6 strategy game. Includes the headless WebSocket game server, the browser client, and Nix packaging (server and web-export derivations plus a NixOS module) for deployment.
103 lines
3.6 KiB
GDScript
103 lines
3.6 KiB
GDScript
class_name MapTopology
|
|
|
|
extends RefCounted
|
|
|
|
## Describes how the tile grid connects at its edges. FLAT keeps every
|
|
## coordinate independent; CYLINDRICAL wraps the east and west edges into one
|
|
## another (the north and south edges stay bounded), so the world behaves like
|
|
## the surface of a cylinder. This class centralises the wrapping maths so the
|
|
## generator, pathfinding, territory and fog all agree on the neighbourhood.
|
|
|
|
enum Type { FLAT, CYLINDRICAL }
|
|
|
|
## Wrapped copies of the world rendered on each side of the real one. Two is
|
|
## enough for the largest zoom the camera allows.
|
|
const COPY_OFFSETS: Array[int] = [-2, -1, 1, 2]
|
|
|
|
var type: Type = Type.FLAT
|
|
|
|
var _size: Vector2i = Vector2i.ZERO
|
|
var _origin: Vector2i = Vector2i.ZERO
|
|
|
|
|
|
func _init(size: Vector2i = Vector2i.ZERO, topology_type: Type = Type.FLAT) -> void:
|
|
_size = size
|
|
_origin = -size / 2
|
|
type = topology_type
|
|
|
|
|
|
func is_cylindrical() -> bool:
|
|
return type == Type.CYLINDRICAL
|
|
|
|
|
|
func width() -> int:
|
|
return _size.x
|
|
|
|
|
|
## Folds a single horizontal coordinate into the map range.
|
|
func wrap_x(value: int) -> int:
|
|
if not is_cylindrical() or _size.x <= 0:
|
|
return value
|
|
return posmod(value - _origin.x, _size.x) + _origin.x
|
|
|
|
|
|
## Folds a coordinate into the map range on the wrapped axis.
|
|
func wrap_coords(coords: Vector2i) -> Vector2i:
|
|
if not is_cylindrical():
|
|
return coords
|
|
return Vector2i(wrap_x(coords.x), coords.y)
|
|
|
|
|
|
## Shortest signed offset from one tile to another, taking the wrap into
|
|
## account. On a flat map this is simply to - from.
|
|
func wrapped_delta(from: Vector2i, to: Vector2i) -> Vector2i:
|
|
var delta := to - from
|
|
if is_cylindrical() and _size.x > 0:
|
|
delta.x = posmod(delta.x + _size.x / 2, _size.x) - _size.x / 2
|
|
return delta
|
|
|
|
|
|
## Euclidean tile distance using the shortest wrapped offset.
|
|
func tile_distance(from: Vector2i, to: Vector2i) -> float:
|
|
return Vector2(wrapped_delta(from, to)).length()
|
|
|
|
|
|
## Returns the neighbours of a cell, folded back into the map when wrapping.
|
|
func surrounding_cells(terrain_layer: TileMapLayer, coords: Vector2i) -> Array[Vector2i]:
|
|
var result: Array[Vector2i] = []
|
|
if not terrain_layer:
|
|
return result
|
|
for neighbour in terrain_layer.get_surrounding_cells(coords):
|
|
result.append(wrap_coords(neighbour))
|
|
return result
|
|
|
|
|
|
## Pixel vector from a tile to another tile, following the shortest wrapped
|
|
## route. The offset is measured between the two real cells (not from the
|
|
## origin) because the hexagonal grid shifts alternate rows horizontally, so
|
|
## the pixel delta depends on where it starts.
|
|
func pixel_delta(terrain_layer: TileMapLayer, from: Vector2i, to: Vector2i) -> Vector2:
|
|
var near := from + wrapped_delta(from, to)
|
|
return terrain_layer.map_to_local(near) - terrain_layer.map_to_local(from)
|
|
|
|
|
|
## Vector from a tile to its wrapped neighbour, which is what border and path
|
|
## drawing need at the seam.
|
|
func neighbour_offset(terrain_layer: TileMapLayer, from: Vector2i, to: Vector2i) -> Vector2:
|
|
return pixel_delta(terrain_layer, from, to)
|
|
|
|
|
|
## Converts a tile-space offset into a pixel offset using the layer's basis.
|
|
## Prefer pixel_delta when a starting cell is known, since the hex grid's row
|
|
## offsets make the result position-dependent.
|
|
func map_delta_to_local(terrain_layer: TileMapLayer, delta: Vector2i) -> Vector2:
|
|
return terrain_layer.map_to_local(delta) - terrain_layer.map_to_local(Vector2i.ZERO)
|
|
|
|
|
|
## Horizontal pixel period of the map: the distance between a tile and the
|
|
## wrapped copy of that same tile.
|
|
func period_pixels(terrain_layer: TileMapLayer) -> Vector2:
|
|
if not is_cylindrical() or _size.x <= 0:
|
|
return Vector2.ZERO
|
|
return map_delta_to_local(terrain_layer, Vector2i(_size.x, 0))
|