Files
Battle-for-Tismo/scripts/classes/map_topology.gd
T
adrien c49899a2fd Initial commit
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.
2026-09-15 14:48:34 +02:00

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))