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