PirateTreasureHunt/addons/gdhexgrid/HexCell.gd

239 lines
7.1 KiB
GDScript
Raw Permalink Normal View History

2021-06-13 15:03:10 +02:00
"""
A single cell of a hexagonal grid.
There are many ways to orient a hex grid, this library was written
with the following assumptions:
* The hexes use a flat-topped orientation;
* Axial coordinates use +x => NE; +y => N;
* Offset coords have odd rows shifted up half a step.
Using x,y instead of the reference's preferred x,z for axial coords makes
following along with the reference a little more tricky, but is less confusing
when using Godot's Vector2(x, y) objects.
## Usage:
#### var cube_coords; var axial_coords; var offset_coords
Cube coordinates are used internally as the canonical representation, but
both axial and offset coordinates can be read and modified through these
properties.
#### func get_adjacent(direction)
Returns the neighbouring HexCell in the given direction.
The direction should be one of the DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, or
DIR_NW constants provided by the HexCell class.
#### func get_all_adjacent()
Returns an array of the six HexCell instances neighbouring this one.
#### func get_all_within(distance)
Returns an array of all the HexCells within the given number of steps,
including the current hex.
#### func get_ring(distance)
Returns an array of all the HexCells at the given distance from the current.
#### func distance_to(target)
Returns the number of hops needed to get from this hex to the given target.
The target can be supplied as either a HexCell instance, cube or axial
coordinates.
#### func line_to(target)
Returns an array of all the hexes crossed when drawing a straight line
between this hex and another.
The target can be supplied as either a HexCell instance, cube or axial
coordinates.
The items in the array will be in the order of traversal, and include both
the start (current) hex, as well as the final target.
"""
extends Resource
#warning-ignore-all:unused_class_variable
# We use unit-size flat-topped hexes
const size = Vector2(1, sqrt(3)/2)
# Directions of neighbouring cells
const DIR_N = Vector3(0, 1, -1)
const DIR_NE = Vector3(1, 0, -1)
const DIR_SE = Vector3(1, -1, 0)
const DIR_S = Vector3(0, -1, 1)
const DIR_SW = Vector3(-1, 0, 1)
const DIR_NW = Vector3(-1, 1, 0)
const DIR_ALL = [DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, DIR_NW]
# Cube coords are canonical
var cube_coords = Vector3(0, 0, 0) setget set_cube_coords, get_cube_coords
# but other coord systems can be used
var axial_coords setget set_axial_coords, get_axial_coords
var offset_coords setget set_offset_coords, get_offset_coords
func _init(coords=null):
# HexCells can be created with coordinates
if coords:
self.cube_coords = obj_to_coords(coords)
func new_hex(coords):
# Returns a new HexCell instance
return get_script().new(coords)
"""
Handle coordinate access and conversion
"""
func obj_to_coords(val):
# Returns suitable cube coordinates for the given object
# The given object can an be one of:
# * Vector3 of standard cube coords;
# * Vector2 of axial coords;
# * HexCell instance
# Any other type of value will return null
#
# NB that offset coords are NOT supported, as they are
# indistinguishable from axial coords.
if typeof(val) == TYPE_VECTOR3:
return val
elif typeof(val) == TYPE_VECTOR2:
return axial_to_cube_coords(val)
elif typeof(val) == TYPE_OBJECT and val.has_method("get_cube_coords"):
return val.get_cube_coords()
# Fall through to nothing
return
func axial_to_cube_coords(val):
# Returns the Vector3 cube coordinates for an axial Vector2
var x = val.x
var y = val.y
return Vector3(x, y, -x - y)
func round_coords(val):
# Rounds floaty coordinate to the nearest whole number cube coords
if typeof(val) == TYPE_VECTOR2:
val = axial_to_cube_coords(val)
# Straight round them
var rounded = Vector3(round(val.x), round(val.y), round(val.z))
# But recalculate the one with the largest diff so that x+y+z=0
var diffs = (rounded - val).abs()
if diffs.x > diffs.y and diffs.x > diffs.z:
rounded.x = -rounded.y - rounded.z
elif diffs.y > diffs.z:
rounded.y = -rounded.x - rounded.z
else:
rounded.z = -rounded.x - rounded.y
return rounded
func get_cube_coords():
# Returns a Vector3 of the cube coordinates
return cube_coords
func set_cube_coords(val):
# Sets the position from a Vector3 of cube coordinates
if abs(val.x + val.y + val.z) > 0.0001:
print("WARNING: Invalid cube coordinates for hex (x+y+z!=0): ", val)
return
cube_coords = round_coords(val)
func get_axial_coords():
# Returns a Vector2 of the axial coordinates
return Vector2(cube_coords.x, cube_coords.y)
func set_axial_coords(val):
# Sets position from a Vector2 of axial coordinates
set_cube_coords(axial_to_cube_coords(val))
func get_offset_coords():
# Returns a Vector2 of the offset coordinates
var x = int(cube_coords.x)
var y = int(cube_coords.y)
var off_y = y + (x - (x & 1)) / 2
return Vector2(x, off_y)
func set_offset_coords(val):
# Sets position from a Vector2 of offset coordinates
var x = int(val.x)
var y = int(val.y)
var cube_y = y - (x - (x & 1)) / 2
self.set_axial_coords(Vector2(x, cube_y))
"""
Finding our neighbours
"""
func get_adjacent(dir):
# Returns a HexCell instance for the given direction from this.
# Intended for one of the DIR_* consts, but really any Vector2 or x+y+z==0 Vector3 will do.
if typeof(dir) == TYPE_VECTOR2:
dir = axial_to_cube_coords(dir)
return new_hex(self.cube_coords + dir)
func get_all_adjacent():
# Returns an array of HexCell instances representing adjacent locations
var cells = Array()
for coord in DIR_ALL:
cells.append(new_hex(self.cube_coords + coord))
return cells
func get_all_within(distance):
# Returns an array of all HexCell instances within the given distance
var cells = Array()
for dx in range(-distance, distance+1):
for dy in range(max(-distance, -distance - dx), min(distance, distance - dx) + 1):
cells.append(new_hex(self.axial_coords + Vector2(dx, dy)))
return cells
func get_ring(distance):
# Returns an array of all HexCell instances at the given distance
if distance < 1:
return [new_hex(self.cube_coords)]
# Start at the top (+y) and walk in a clockwise circle
var cells = Array()
var current = new_hex(self.cube_coords + (DIR_N * distance))
for dir in [DIR_SE, DIR_S, DIR_SW, DIR_NW, DIR_N, DIR_NE]:
for _step in range(distance):
cells.append(current)
current = current.get_adjacent(dir)
return cells
func distance_to(target):
# Returns the number of hops from this hex to another
# Can be passed cube or axial coords, or another HexCell instance
target = obj_to_coords(target)
return int((
abs(cube_coords.x - target.x)
+ abs(cube_coords.y - target.y)
+ abs(cube_coords.z - target.z)
) / 2)
func line_to(target):
# Returns an array of HexCell instances representing
# a straight path from here to the target, including both ends
target = obj_to_coords(target)
# End of our lerp is nudged so it never lands exactly on an edge
var nudged_target = target + Vector3(1e-6, 2e-6, -3e-6)
var steps = distance_to(target)
var path = []
for dist in range(steps):
var lerped = cube_coords.linear_interpolate(nudged_target, float(dist) / steps)
path.append(new_hex(round_coords(lerped)))
path.append(new_hex(target))
return path