402 lines
14 KiB
GDScript
402 lines
14 KiB
GDScript
extends Node3D
|
|
class_name HexGrid
|
|
|
|
@onready var placement_visualizer = $PlacementVisualizer
|
|
|
|
const DIR_N: Vector3 = Vector3(0, 1, -1)
|
|
const DIR_NE: Vector3 = Vector3(1, 0, -1)
|
|
const DIR_SE: Vector3 = Vector3(1, -1, 0)
|
|
const DIR_S: Vector3 = Vector3(0, -1, 1)
|
|
const DIR_SW: Vector3 = Vector3(-1, 0, 1)
|
|
const DIR_NW: Vector3 = Vector3(-1, 1, 0)
|
|
const DIR_ALL: Array[Vector3] = [DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, DIR_NW]
|
|
|
|
#const size = Vector2(1, sqrt(3.0)/2.0)
|
|
|
|
const size: float = 0.5
|
|
|
|
var used_cells: Dictionary = {}
|
|
|
|
@export var layer_height: float = 0.4
|
|
|
|
# have all used_cells be saved as Vector4i (q, r, s, y)
|
|
|
|
class TileStorage:
|
|
var tiles: Dictionary = {}
|
|
# we use a vector4i for coordinates
|
|
# q r s y (layer)
|
|
|
|
func add_tile(tile: InsectTile, coords: CubeCoordinates, layer: int = 0) -> void:
|
|
tiles[Vector4i(coords.q, coords.r, coords.s, layer)] = tile
|
|
pass
|
|
|
|
func remove_tile(coords: CubeCoordinates, layer: int = 0) -> void:
|
|
pass
|
|
|
|
func has_tile(coords: CubeCoordinates, layer: int = 0) -> bool:
|
|
return tiles.has(Vector4i(coords.q, coords.r, coords.s, layer))
|
|
|
|
func get_tile(coords: CubeCoordinates, layer: int = 0) -> InsectTile:
|
|
return tiles[Vector4i(coords.q, coords.r, coords.s, layer)]
|
|
|
|
|
|
class CubeCoordinates:
|
|
var q: float
|
|
var r: float
|
|
var s: float
|
|
|
|
func _init(_q: float, _r: float, _s: float):
|
|
q = _q
|
|
r = _r
|
|
s = _s
|
|
|
|
class AxialCoordinates:
|
|
var q: float
|
|
var r: float
|
|
|
|
func _init(_q: float, _r: float):
|
|
q = _q
|
|
r = _r
|
|
|
|
func flat_hex_corner(center: Vector2, size: float, corner_num: int) -> Vector2:
|
|
var angle_deg: int = 60 * corner_num
|
|
var angle_rad: float = deg_to_rad(angle_deg)
|
|
return Vector2(center.x + size * cos(angle_rad), center.y + size * sin(angle_rad))
|
|
|
|
func flat_hex_to_world_position(coords: AxialCoordinates) -> Vector2:
|
|
var x = size * (3.0/2.0 * coords.q)
|
|
var y = size * (sqrt(3.0)/2.0 * coords.q + sqrt(3.0) * coords.r)
|
|
return Vector2(x, y)
|
|
|
|
func cube_to_world_pos(coords: CubeCoordinates) -> Vector2:
|
|
return flat_hex_to_world_position(cube_to_axial(coords))
|
|
|
|
#func world_to_hex_tile(world_pos: Vector3) -> Vector2:
|
|
# var q = (2.0/3.0 * world_pos.x)
|
|
# var r = (-1.0/3.0 * world_pos.x + sqrt(3.0)/3.0 * world_pos.z)
|
|
#
|
|
# return cube_round()
|
|
#
|
|
# return
|
|
|
|
const INSECT_TILE = preload("res://InsectTiles/InsectTile.tscn")
|
|
|
|
func world_to_hex_tile(coords: Vector2) -> AxialCoordinates:
|
|
var q = (2.0/3.0 * coords.x) / size
|
|
var r = (-1.0/3.0 * coords.x + sqrt(3.0)/3.0 * coords.y) / size
|
|
return axial_round(AxialCoordinates.new(q, r))
|
|
|
|
func axial_to_cube(coords: AxialCoordinates) -> CubeCoordinates:
|
|
var q = coords.q
|
|
var r = coords.r
|
|
var s = -q-r
|
|
return CubeCoordinates.new(q, r, s)
|
|
|
|
func cube_to_axial(coords: CubeCoordinates) -> AxialCoordinates:
|
|
var q = coords.q
|
|
var r = coords.r
|
|
return AxialCoordinates.new(q, r)
|
|
|
|
func axial_round(coords: AxialCoordinates) -> AxialCoordinates:
|
|
return cube_to_axial(cube_round(axial_to_cube(coords)))
|
|
|
|
func cube_round(coords: CubeCoordinates) -> CubeCoordinates:
|
|
var q: float = round(coords.q)
|
|
var r: float = round(coords.r)
|
|
var s: float = round(coords.s)
|
|
|
|
var q_diff: float = abs(q - coords.q)
|
|
var r_diff: float = abs(r - coords.r)
|
|
var s_diff: float = abs(s - coords.s)
|
|
|
|
if q_diff > r_diff and q_diff > s_diff:
|
|
q = -r-s
|
|
elif r_diff > s_diff:
|
|
r = -q-s
|
|
else:
|
|
s = -q-r
|
|
|
|
return CubeCoordinates.new(q, r, s)
|
|
|
|
@export var dragging_intersect_plane_normal: Vector3 = Vector3.UP
|
|
@export var dragging_intersect_plane_distance: float = 0.0
|
|
|
|
func get_3d_pos(position2D: Vector2):
|
|
return Plane(dragging_intersect_plane_normal, dragging_intersect_plane_distance).intersects_ray(get_viewport().get_camera_3d().project_ray_origin(position2D), get_viewport().get_camera_3d().project_ray_normal(position2D))
|
|
|
|
var placements: Dictionary = {}
|
|
|
|
func is_cell_empty(coords: CubeCoordinates) -> bool:
|
|
return !used_cells.has(Vector4i(coords.q, coords.r, coords.s, 0))
|
|
|
|
func is_cell_not_empty(coords: CubeCoordinates) -> bool:
|
|
return !is_cell_empty(coords)
|
|
|
|
func get_empty_neighbours(coords: CubeCoordinates) -> Array[CubeCoordinates]:
|
|
return get_neighbours(coords).filter(is_cell_empty)
|
|
|
|
func get_neighbours(coords: CubeCoordinates) -> Array[CubeCoordinates]:
|
|
return [
|
|
CubeCoordinates.new(coords.q + 1, coords.r, coords.s - 1),
|
|
CubeCoordinates.new(coords.q + 1, coords.r - 1, coords.s),
|
|
CubeCoordinates.new(coords.q, coords.r - 1, coords.s + 1),
|
|
CubeCoordinates.new(coords.q - 1, coords.r, coords.s + 1),
|
|
CubeCoordinates.new(coords.q - 1, coords.r + 1, coords.s),
|
|
CubeCoordinates.new(coords.q, coords.r + 1, coords.s - 1)
|
|
]
|
|
|
|
var current_tile: Node3D
|
|
|
|
const HEX_OUTLINE = preload("res://hex_outline.tscn")
|
|
|
|
func _on_insect_selected(insect_resource: TileResource, is_black: bool) -> void:
|
|
# create a hexagon with insect resource data
|
|
#var tile = INSECT_TILE.instantiate()
|
|
#tile.resource = insect_resource
|
|
#tile.is_black = is_black
|
|
#current_tile = tile
|
|
#add_child(tile)
|
|
|
|
# spawn possible placement locations :)
|
|
if used_cells.size() == 0: # we have no cells placed, display a placement outline at 0, 0
|
|
var outline = HEX_OUTLINE.instantiate()
|
|
var cubepos = CubeCoordinates.new(0, 0, 0)
|
|
var hex_pos = cube_to_world_pos(cubepos)
|
|
outline.position = Vector3(hex_pos.x, 0.0, hex_pos.y)
|
|
outline.visible = true
|
|
outline.insect_resource = insect_resource
|
|
outline.is_black = is_black
|
|
outline.coordinates = cubepos
|
|
placement_visualizer.add_child(outline)
|
|
placements[hex_pos] = outline
|
|
elif used_cells.size() == 1: # we have ONE cell placed, this is a special case in which
|
|
# the opposing player is allowed to place a tile that touches the enemy color
|
|
# We display outline placement around all spaces of this single cell
|
|
var single_cell = used_cells.keys().front()
|
|
var neighbours = get_neighbours(CubeCoordinates.new(single_cell.x, single_cell.y, single_cell.z))
|
|
for neighbour in neighbours:
|
|
var outline = HEX_OUTLINE.instantiate()
|
|
var hex_pos = cube_to_world_pos(neighbour)
|
|
outline.position = Vector3(hex_pos.x, 0.0, hex_pos.y)
|
|
outline.visible = true
|
|
outline.insect_resource = insect_resource
|
|
outline.is_black = is_black
|
|
outline.coordinates = neighbour
|
|
placement_visualizer.add_child(outline)
|
|
placements[hex_pos] = outline
|
|
else:
|
|
# iterate over all used_cells, get all empty cells surrounding those cells
|
|
# iterate over all those empty cells, check if they only neighbour the same color
|
|
var possible_placements: Dictionary = {}
|
|
|
|
for hex in used_cells.keys():
|
|
#var eligible: bool = true
|
|
for neighbour in get_empty_neighbours(CubeCoordinates.new(hex.x, hex.y, hex.z)):
|
|
if not used_cells.has(Vector4(neighbour.q, neighbour.r, neighbour.s, 0)):
|
|
possible_placements[Vector4i(neighbour.q, neighbour.r, neighbour.s, 0)] = true
|
|
|
|
for p in possible_placements:
|
|
#var neighbours = [
|
|
# Vector2i(p.x + 1, p.y), Vector2i(p.x + 1, p.y - 1), Vector2i(p.x, p.y - 1),
|
|
# Vector2i(p.x - 1, p.y), Vector2i(p.x - 1, p.y + 1), Vector2i(p.x, p.y + 1)
|
|
#]
|
|
|
|
var eligible: bool = true
|
|
|
|
for neighbour in get_neighbours(CubeCoordinates.new(p.x, p.y, p.z)):
|
|
if not used_cells.has(Vector4i(neighbour.q, neighbour.r, neighbour.s, 0)):
|
|
continue
|
|
|
|
if used_cells[Vector4i(neighbour.q, neighbour.r, neighbour.s, 0)].is_black != is_black:
|
|
eligible = false
|
|
break
|
|
|
|
if eligible:
|
|
var outline = HEX_OUTLINE.instantiate()
|
|
var hex_pos = cube_to_world_pos(CubeCoordinates.new(p.x, p.y, p.z))
|
|
outline.position = Vector3(hex_pos.x, 0.0, hex_pos.y)
|
|
outline.visible = true
|
|
outline.insect_resource = insect_resource
|
|
outline.is_black = is_black
|
|
outline.coordinates = CubeCoordinates.new(p.x, p.y, p.z)
|
|
placement_visualizer.add_child(outline)
|
|
placements[p] = outline
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
func _on_insect_placement_cancelled() -> void:
|
|
if current_tile:
|
|
current_tile.queue_free()
|
|
current_tile = null
|
|
|
|
for child in placement_visualizer.get_children():
|
|
child.queue_free()
|
|
|
|
func _on_insect_placed(resource: TileResource, is_black: bool, pos: CubeCoordinates) -> void:
|
|
var tile_copy = INSECT_TILE.instantiate()
|
|
var hex_pos = cube_to_world_pos(pos)
|
|
|
|
tile_copy.position = Vector3(hex_pos.x, 20.0, hex_pos.y)
|
|
tile_copy.resource = resource
|
|
tile_copy.is_black = is_black
|
|
tile_copy.coordinates = pos
|
|
var target_pos = Vector3(hex_pos.x, 0.0, hex_pos.y)
|
|
|
|
used_cells[Vector4i(pos.q, pos.r, pos.s, 0)] = tile_copy
|
|
|
|
add_child(tile_copy)
|
|
|
|
var tween = get_tree().create_tween()
|
|
tween.tween_property(tile_copy, "position", target_pos, 1.0).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_EXPO)
|
|
|
|
func _on_insect_tile_selected(tile: InsectTile) -> void:
|
|
if tile.resource.movement_behaviour == null:
|
|
return
|
|
|
|
var spaces = tile.resource.movement_behaviour.get_available_spaces(tile.coordinates, self)
|
|
|
|
print(spaces.size())
|
|
|
|
if spaces.is_empty():
|
|
GameEvents.insect_tile_selection_request_failed.emit(tile)
|
|
return
|
|
|
|
for space in spaces:
|
|
var neighbours = get_neighbours(space)
|
|
# if all neighbours are empty, move would disconnect us from the hive
|
|
# so we discard it
|
|
# also: if there are 5 empty space and the only remaining one is our current tile...
|
|
# we would also be disconnected after the move
|
|
# maybe the 6 check is not needed
|
|
var non_empty_neighbours = neighbours.filter(is_cell_not_empty)
|
|
var coords_vec4: Vector4i = Vector4i(tile.coordinates.q, tile.coordinates.r, tile.coordinates.s, 0)
|
|
|
|
if non_empty_neighbours.size() == 1:
|
|
var occupied_neighbour = non_empty_neighbours.front()
|
|
var neighbour_vec4: Vector4i = Vector4i(occupied_neighbour.q, occupied_neighbour.r, occupied_neighbour.s, 0)
|
|
if neighbour_vec4 == coords_vec4:
|
|
continue
|
|
|
|
var outline = HEX_OUTLINE.instantiate()
|
|
var hex_pos = cube_to_world_pos(space) # flat_hex_to_world_position(AxialCoordinates.new(space.x, space.y))
|
|
outline.position = Vector3(hex_pos.x, 0.0, hex_pos.y)
|
|
outline.coordinates = space
|
|
outline.visible = true
|
|
outline.insect_tile = tile
|
|
outline.is_moving = true
|
|
outline.insect_resource = tile.resource
|
|
outline.is_black = tile.is_black
|
|
placement_visualizer.add_child(outline)
|
|
placements[space] = outline
|
|
|
|
|
|
func _on_insect_tile_moved(tile: InsectTile, target: CubeCoordinates) -> void:
|
|
used_cells.erase(Vector4i(tile.coordinates.q, tile.coordinates.r, tile.coordinates.s, 0))
|
|
|
|
var new_hex_pos = cube_to_world_pos(target)
|
|
var sky_new_hex_pos = Vector3(new_hex_pos.x, 20.0, new_hex_pos.y)
|
|
var ground_new_hex_pos = Vector3(new_hex_pos.x, 0.0, new_hex_pos.y)
|
|
#
|
|
var current_hex_pos = tile.position
|
|
var sky_current_hex_pos = tile.position + Vector3(0.0, 20.0, 0.0)
|
|
#
|
|
var tween = get_tree().create_tween()
|
|
tween.tween_property(tile, "position", sky_current_hex_pos, 0.5).set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_EXPO)
|
|
tween.tween_property(tile, "position", sky_new_hex_pos, 0.0)
|
|
tween.tween_property(tile, "position", ground_new_hex_pos, 1.0).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_EXPO)
|
|
|
|
tile.coordinates = target
|
|
|
|
used_cells[Vector4i(target.q, target.r, target.s, 0)] = tile
|
|
|
|
func can_hive_exist_without_tile(tile: InsectTile) -> bool:
|
|
# TODO: BFS-Search from random cell to see if all other cells could still be reached when this
|
|
# tile would be empty space
|
|
if get_empty_neighbours(tile.coordinates).size() == 5: # we only have one real neighbour, so can't break anything
|
|
return true
|
|
|
|
var vector_coords: Vector4i = Vector4i(tile.coordinates.q, tile.coordinates.r, tile.coordinates.s, 0)
|
|
|
|
# DO BFS
|
|
var tiles_reached: Array = []
|
|
var tiles_available: Array = used_cells.keys().filter(func(coords): return coords != vector_coords)
|
|
|
|
if tiles_available.size() <= 1:
|
|
# If we only have 1 or 2 total tiles, we can always move
|
|
# 1 tile should never happen
|
|
# two... could theoretically but yeah
|
|
return true
|
|
|
|
# tiles_available has all remaining tiles, we just need to visit every tile from a (random) starting tile
|
|
# and compare the size with these of all tiles - 1 (our to be moved one)
|
|
var start: Vector4i = tiles_available.front()
|
|
tiles_reached.push_back(start)
|
|
var queue: Array[Vector4i] = [start]
|
|
|
|
while queue.size() > 0:
|
|
var m = queue.pop_front()
|
|
|
|
for neighbour in get_neighbours(CubeCoordinates.new(m.x, m.y, m.z)):
|
|
var neighbour_vec4: Vector4i = Vector4i(neighbour.q, neighbour.r, neighbour.s, 0)
|
|
if neighbour_vec4 not in tiles_reached and neighbour_vec4 != vector_coords:
|
|
if used_cells.has(neighbour_vec4):
|
|
tiles_reached.push_back(neighbour_vec4)
|
|
queue.push_back(neighbour_vec4)
|
|
|
|
return tiles_reached.size() == used_cells.size() - 1
|
|
|
|
func _on_insect_tile_request_selection(tile: InsectTile) -> void:
|
|
if can_hive_exist_without_tile(tile):
|
|
GameEvents.insect_tile_selection_request_successful.emit(tile)
|
|
else:
|
|
GameEvents.insect_tile_selection_request_failed.emit(tile)
|
|
|
|
func _ready() -> void:
|
|
GameEvents.insect_selected.connect(_on_insect_selected)
|
|
GameEvents.insect_placement_cancelled.connect(_on_insect_placement_cancelled)
|
|
GameEvents.insect_placed.connect(_on_insect_placed)
|
|
GameEvents.insect_tile_selected.connect(_on_insect_tile_selected)
|
|
GameEvents.insect_tile_moved.connect(_on_insect_tile_moved)
|
|
GameEvents.insect_tile_request_selection.connect(_on_insect_tile_request_selection)
|
|
|
|
#func spawn_random_tile() -> void:
|
|
#var tile_copy = hex.duplicate()
|
|
#var hex_pos = flat_hex_to_world_position(AxialCoordinates.new(randi_range(-20, 20), randi_range(-20, 20)))
|
|
#
|
|
#tile_copy.position = Vector3(hex_pos.x, 20.0, hex_pos.y)
|
|
#var target_pos = Vector3(hex_pos.x, 0.0, hex_pos.y)
|
|
#
|
|
#add_child(tile_copy)
|
|
#
|
|
#var tween = get_tree().create_tween()
|
|
#tween.tween_property(tile_copy, "position", target_pos, 1.0).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_EXPO)
|
|
#
|
|
#func move_tile_to_random_position() -> void:
|
|
#var new_hex_pos = flat_hex_to_world_position(AxialCoordinates.new(randi_range(-20, 20), randi_range(-20, 20)))
|
|
#var sky_new_hex_pos = Vector3(new_hex_pos.x, 20.0, new_hex_pos.y)
|
|
#var ground_new_hex_pos = Vector3(new_hex_pos.x, 0.0, new_hex_pos.y)
|
|
#
|
|
#var current_hex_pos = hex.position
|
|
#var sky_current_hex_pos = hex.position + Vector3(0.0, 20.0, 0.0)
|
|
#
|
|
#var tween = get_tree().create_tween()
|
|
#tween.tween_property(hex, "position", sky_current_hex_pos, 0.5).set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_EXPO)
|
|
#tween.tween_property(hex, "position", sky_new_hex_pos, 0.0)
|
|
#tween.tween_property(hex, "position", ground_new_hex_pos, 1.0).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_EXPO)
|
|
|
|
func _process(delta) -> void:
|
|
#if Input.is_action_just_pressed("ui_accept"):
|
|
# print("yay")
|
|
# spawn_random_tile()
|
|
return
|
|
|
|
if current_tile == null:
|
|
return
|
|
|
|
#var pos3d = get_3d_pos(get_viewport().get_mouse_position())
|
|
#if pos3d:
|
|
#var hex_pos = flat_hex_to_world_position(world_to_hex_tile(Vector2(pos3d.x, pos3d.z)))
|
|
#current_tile.position = Vector3(hex_pos.x, 0.0, hex_pos.y)
|
|
#coord_label.text = "%d, %d" % [hex_pos.x, hex_pos.y]
|