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) # We can hover all of our tiles (will show on-hover shader) # If we click on a placed tile, we do the following # Check if we would split the hive -> deny selection # Get possible movement tiles (via MovementBehaviour) # If 0 -> Deny # Filter remaining movement spots to not include hive-splitting moves(?) 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 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: Vector4i) -> 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 cube_to_axial(coords: Vector4i) -> AxialCoordinates: var q = coords.x var r = coords.y return AxialCoordinates.new(q, r) func axial_round(coords: AxialCoordinates) -> AxialCoordinates: return cube_to_axial(cube_round(axial_to_cube(coords))) func axial_to_cube(coords: AxialCoordinates) -> Vector4i: var q = coords.q var r = coords.r var s = -q-r return Vector4i(q, r, s, 0) func cube_round(coords: Vector4i) -> Vector4i: var q: float = round(coords.x) var r: float = round(coords.y) var s: float = round(coords.z) var q_diff: float = abs(q - coords.x) var r_diff: float = abs(r - coords.y) var s_diff: float = abs(s - coords.z) 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 Vector4i(q, r, s, 0) @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: Vector4i) -> bool: return !used_cells.has(coords) func is_cell_not_empty(coords: Vector4i) -> bool: return !is_cell_empty(coords) func get_empty_neighbours(coords: Vector4i) -> Array[Vector4i]: return get_neighbours(coords).filter(is_cell_empty) func get_highest_in_stack(coords: Vector4i) -> Vector4i: if not used_cells.has(coords): #print("ground") return coords var top: InsectTile = used_cells[coords] while top.hat != null: top = top.hat #print("found top") #print(top.coordinates) return top.coordinates func get_neighbours(coords: Vector4i, ground_layer: bool = false) -> Array[Vector4i]: var layer: int = coords.w if ground_layer: layer = 0 return [ Vector4i(coords.x + 1, coords.y, coords.z - 1, layer), Vector4i(coords.x + 1, coords.y - 1, coords.z, layer), Vector4i(coords.x, coords.y - 1, coords.z + 1, layer), Vector4i(coords.x - 1, coords.y, coords.z + 1, layer), Vector4i(coords.x - 1, coords.y + 1, coords.z, layer), Vector4i(coords.x, coords.y + 1, coords.z - 1, layer) ] const HEX_OUTLINE = preload("res://hex_outline.tscn") func get_placeable_positions(button: InsectButton) -> Array[Vector4i]: if used_cells.size() == 0: return [Vector4i.ZERO] elif used_cells.size() == 1: var single_cell = used_cells.keys().front() var neighbours = get_neighbours(single_cell) var positions: Array[Vector4i] = [] for neighbour in neighbours: #var hex_pos = cube_to_world_pos(neighbour) positions.push_back(neighbour) return positions var possible_placements: Dictionary = {} var positions: Array[Vector4i] = [] for hex in used_cells.keys().filter(func(coords): return coords.w == 0): # We filter here because we only want neighbours of ground level tiles # Otherwise we spawn an oultine on ground level but with an internal # Coordinate of somewhere higher up # Visually everything will be correct but internally the resulting hex # Will count as higher layer tile leading to nasty bugs for neighbour in get_empty_neighbours(hex): if not used_cells.has(neighbour): possible_placements[neighbour] = true for p in possible_placements: var eligible: bool = true for neighbour in get_neighbours(p): if not used_cells.has(neighbour): continue if used_cells[get_highest_in_stack(neighbour)].is_black != button.is_black: eligible = false break if eligible: positions.push_back(p) return positions func get_left_neighbour(pos: Vector4i) -> Vector4i: return Vector4i(-pos.z, -pos.x, -pos.y, 0) func get_right_neighbour(pos: Vector4i) -> Vector4i: return Vector4i(-pos.y, -pos.z, -pos.x, 0) var debug_labels = [] func debug_label(pos, text) -> void: var label = Label3D.new() label.billboard = BaseMaterial3D.BILLBOARD_ENABLED label.text = text label.no_depth_test = true var p = cube_to_world_pos(pos) label.position = Vector3(p.x, 0.2, p.y) add_child(label) debug_labels.push_back(label) func clear_debug_labels() -> void: for label in debug_labels: label.queue_free() debug_labels.clear() func can_reach(start: Vector4i, target: Vector4i, exclude: Array[Vector4i] = []) -> bool: if start.w != target.w: return true # if we have 5 potential spaces it can never be blocked var offset: Vector4i = Vector4i.ZERO offset.x = target.x - start.x offset.y = target.y - start.y offset.z = target.z - start.z var left = get_left_neighbour(offset) var right = get_right_neighbour(offset) var left_coord = Vector4i(left.x + start.x, left.y + start.y, left.z + start.z, start.w) var right_coord = Vector4i(right.x + start.x, right.y + start.y, right.z + start.z, start.w) if left_coord in exclude or right_coord in exclude: #print("excluded?") return true return is_cell_empty(left_coord) or is_cell_empty(right_coord) func _on_insect_selected(button: InsectButton, is_black: bool) -> void: var positions = get_placeable_positions(button) for child in placement_visualizer.get_children(): child.queue_free() for p in positions: var outline = HEX_OUTLINE.instantiate() var hex_pos = cube_to_world_pos(p) outline.position = Vector3(hex_pos.x, 0.0, hex_pos.y) outline.visible = true outline.insect_resource = button.insect_resource outline.is_black = is_black outline.coordinates = p outline.map_reference = self placement_visualizer.add_child(outline) func _on_insect_placement_cancelled() -> void: for child in placement_visualizer.get_children(): child.queue_free() @rpc("any_peer", "call_local") func place_insect_tile(resource_path: String, is_black: bool, pos: Vector4i) -> void: var resource = load(resource_path) 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 #print(pos) tile_copy.map_reference = self var target_pos = Vector3(hex_pos.x, 0.0, hex_pos.y) used_cells[pos] = tile_copy var sender_id = multiplayer.get_remote_sender_id() tile_copy.set_multiplayer_authority(sender_id) add_child(tile_copy) GameEvents.insect_tile_created.emit(tile_copy, pos) 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_placed(resource: TileResource, is_black: bool, pos: Vector4i) -> void: var resource_path = resource.resource_path place_insect_tile.rpc(resource_path, is_black, pos) func can_move(tile: InsectTile) -> bool: if not can_hive_exist_without_tile(tile): return false var spaces = tile.resource.movement_behaviour.get_available_spaces(tile.coordinates, self) if spaces.is_empty(): return false return true func has_action_targets(tile: InsectTile) -> bool: if tile.resource.action_behaviour == null: return false var targets: Array[InsectTile] = tile.resource.action_behaviour.get_targets(tile.coordinates, self) return !targets.is_empty() func _on_insect_tile_action_started(tile: InsectTile) -> void: do_action(tile) func _on_insect_tile_move_started(tile: InsectTile) -> void: do_move(tile) func do_action(tile: InsectTile) -> void: pass func do_move(tile: InsectTile) -> void: for child in placement_visualizer.get_children(): child.queue_free() var spaces = tile.resource.movement_behaviour.get_available_spaces(tile.coordinates, self) if spaces.is_empty(): print("empty?") #GameEvents.insect_tile_selection_request_failed.emit(tile) return for space in spaces: var neighbours = get_neighbours(space) var non_empty_neighbours = neighbours.filter(is_cell_not_empty) if non_empty_neighbours.size() == 1: # NOTE: This is correct! But seemed wrong when testing the beetle movement # If there are only two tiles (beetle + some other) the beetle can't climb ontop of the other # tile. (would not necessarily split the hive, but the simple logic I use thinks so). # This fixes itself automatically when there are more than two tiles present # And since you can't move unless you place the bee, there will always be at least 3 tiles # before you can move your beetle # NOTE: This MIGHT result in a bug when you stack beetles... but no I don't think so # You'd need to have a bee to be able to move, so yeah var occupied_neighbour = non_empty_neighbours.front() if occupied_neighbour == tile.coordinates: continue var layer: int = 0 var temp_tile: InsectTile = null if is_cell_not_empty(space): temp_tile = used_cells.get(space) layer = 1 while temp_tile.hat != null: layer += 1 temp_tile = temp_tile.hat #print(layer) space.w = layer 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, layer * layer_height, 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_selected(tile: InsectTile) -> void: # check the following: # if the insect is selectable for an action (has action_callback data set) -> do that action # else: # if the unit has no action # try moving # else: # show action/move panel, grey out action/or move if there are no target or can't move? if tile.action_callback.is_valid(): tile.action_callback.call() return else: # if tile has an action if tile.resource.action_behaviour != null: GameEvents.choose_action_or_move.emit(tile, has_action_targets(tile), can_move(tile)) return if can_move(tile): do_move(tile) func get_tile(pos: Vector4i) -> InsectTile: return used_cells.get(pos) @rpc("any_peer", "call_local") func move_insect_tile(tile_coords: Vector4i, target: Vector4i) -> void: var tile: InsectTile = used_cells[tile_coords] # Remove from old stack if tile.coordinates.w > 0: var below: Vector4i = tile.coordinates + Vector4i(0, 0, 0, -1) used_cells[below].hat = null used_cells.erase(tile.coordinates) 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, target.w * layer_height, 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[tile.coordinates] = tile if tile.coordinates.w > 0: var below: Vector4i = tile.coordinates + Vector4i(0, 0, 0, -1) used_cells[below].hat = tile GameEvents.insect_tile_finished_moving.emit(tile, target) func _on_insect_tile_moved(tile: InsectTile, target: Vector4i) -> void: move_insect_tile.rpc(tile.coordinates, target) func get_same_neighbours(cell1: Vector4i, cell2: Vector4i) -> Array[Vector4i]: var neighbours1 = get_neighbours(cell1).filter(is_cell_not_empty) var neighbours2 = get_neighbours(cell2).filter(is_cell_not_empty) var shared_neighbours: Array[Vector4i] = [] for n1 in neighbours1: for n2 in neighbours2: if n1 == n2: if n1 not in shared_neighbours: shared_neighbours.push_back(n1) return shared_neighbours func is_position_on_hive(pos: Vector4i) -> bool: return get_empty_neighbours(pos).size() < 6 func can_hive_exist_without_tile(tile: InsectTile) -> bool: if tile.coordinates.w != 0: print("w") return true # 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 print("neighbours") return true # DO BFS var tiles_reached: Array = [] var tiles_available: Array = used_cells.keys().filter(func(coords): return coords != tile.coordinates).filter(func(coords): return coords.w == 0) #print(tiles_available) 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(m): if neighbour not in tiles_reached and neighbour != tile.coordinates: if used_cells.has(neighbour): tiles_reached.push_back(neighbour) queue.push_back(neighbour) return tiles_reached.size() == tiles_available.size() 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_action_started.connect(_on_insect_tile_action_started) GameEvents.insect_tile_move_started.connect(_on_insect_tile_move_started)