extends Node2D @onready var ground_layer: TileMapLayer = $GroundLayer @onready var path_layer: TileMapLayer = $PathLayer @onready var data_layer: TileMapLayer = $DataLayer @onready var navigation_graph: MPNavigationGraph = $NavigationGraph # @onready var player: CharacterBody2D = $Player var last_dice_result: int = 0 # var global_transform_size: Vector2 = Vector2(0, 0) # const DIRECTIONS: Dictionary = { "up": Vector2(0, -1), "down": Vector2(0, 1), "left": Vector2(-1, 0), "right": Vector2(1, 0) } var node_positions: Dictionary = {} func _ready() -> void: global_transform_size = Vector2(16, 16) * data_layer.transform.get_scale() build_graph() # place the player at the first node var current_player_position: Vector2 = node_positions.keys()[0] player.global_position = local_to_world(current_player_position) # bring player to top player.z_index = 1 roll_dice() player.finished_movement.connect(roll_dice) func _physics_process(delta: float) -> void: if Input.is_action_just_pressed("draw_toggle_nodes") or Input.is_action_just_pressed("draw_toggle_edges"): navigation_graph.draw_nodes = !navigation_graph.draw_nodes navigation_graph.draw_edges = !navigation_graph.draw_edges navigation_graph.queue_redraw() var dice_controls_min: int = 2 var dice_controls_max: int = 8 func roll_dice() -> void: last_dice_result = randi() % (dice_controls_max - dice_controls_min + 1) + dice_controls_min print("Dice result: ", last_dice_result) $ResultLabel.text = "Rolled a " + str(last_dice_result) # Find possible movement options var movement_options: Array[MPNavigationNode] = find_nodes_with_distance(world_to_local(player.global_position), last_dice_result) # visualize by spawning an indicator at each possible node, with a click event to move the player for node in movement_options: # res://scenes/mario-party/TileIndicator.tscn var indicator_scene: Node2D = preload("res://scenes/mario-party/TileIndicator.tscn").instantiate() indicator_scene.scale = Vector2(2, 2) var area_node: Area2D = indicator_scene.get_node("area") area_node.set_data(node.position, "player_movement") area_node.indicator_clicked.connect(_on_indicator_clicked) indicator_scene.global_position = local_to_world(node.get_position()) add_child(indicator_scene) func _on_indicator_clicked(pos: Vector2, type: String) -> void: print("Indicator clicked: ", type, " ", pos) if type == "player_movement": player.move_to(pos) # delete all indicators for child in get_children(): if child is Node2D: var area_node: Area2D = child.get_node("area") if area_node != null: if area_node.get_type() == "player_movement": child.queue_free() func local_to_world(location: Vector2) -> Vector2: return location * global_transform_size + global_transform_size / 2 func world_to_local(location: Vector2) -> Vector2: return (location - global_transform_size / 2) / global_transform_size func build_graph() -> void: print("Identifying nodes") # Step 1: Place nodes at positions where is_tile is true for position in data_layer.get_used_cells(): var tile_data: TileData = data_layer.get_cell_tile_data(position) var is_tile: bool = tile_data.get_custom_data("is_tile") if is_tile: var node: MPNavigationNode = navigation_graph.add_node(position.x, position.y) node_positions[position] = node var indicator_scene: Node2D = preload("res://scenes/mario-party/TileIndicator.tscn").instantiate() var area_node: Area2D = indicator_scene.get_node("area") area_node.set_display_type("node") indicator_scene.global_position = local_to_world(position) add_child(indicator_scene) # Step 2: Connect nodes using flood-fill based on walkable tiles print("Connecting nodes") for position in node_positions.keys(): connect_node(position) func connect_node(start_position: Vector2) -> void: var start_node = node_positions.get(Vector2i(start_position)) var visited: Dictionary = {} visited[start_position] = true # print("Connecting node at ", start_position) # For each direction, perform flood-fill for dir_name in DIRECTIONS.keys(): var direction = DIRECTIONS[dir_name] var next_position = start_position + direction # print("Checking direction ", dir_name, " from ", start_position, " to ", next_position) # Ensure the first tile respects the direction if not is_valid_direction(next_position, dir_name): continue # print("Flood-fill in direction ", dir_name, " from ", next_position) # Perform flood-fill from the valid tile var connected_nodes: Array = flood_fill(next_position, visited) # Add connections between the start node and found nodes for target_position in connected_nodes: if target_position != start_position: if node_positions.has(Vector2i(target_position)): var target_node = node_positions.get(Vector2i(target_position)) navigation_graph.add_connection(start_node, target_node) print(start_position, " --> ", target_position) func flood_fill(start_position: Vector2, visited: Dictionary) -> Array: var stack: Array[Vector2] = [start_position] var connected_nodes: Array[Vector2] = [] while stack.size() > 0: var current_position = stack.pop_back() # print(" - Visiting ", current_position) # Skip if already visited if visited.has(current_position): continue visited[current_position] = true # Skip if not walkable var tile_data: TileData = data_layer.get_cell_tile_data(current_position) if tile_data == null: continue var is_walkable: bool = tile_data.get_custom_data("is_walkable") var is_tile: bool = tile_data.get_custom_data("is_tile") if (not is_walkable) and (not is_tile): continue # If this position is a node, add it to the result if is_tile: # print(" - Found node tile at ", current_position) connected_nodes.append(current_position) # Add neighboring tiles to the stack if they respect the direction for dir_name in DIRECTIONS.keys(): var direction = DIRECTIONS[dir_name] var neighbor_position = current_position + direction if not visited.has(neighbor_position): if is_valid_direction(current_position, dir_name): stack.append(neighbor_position) return connected_nodes func is_valid_direction(position: Vector2, required_dir: String) -> bool: var tile_data: TileData = data_layer.get_cell_tile_data(position) if tile_data == null: return false var is_walkable: bool = tile_data.get_custom_data("is_walkable") var walk_dir: String = tile_data.get_custom_data("walk_dir") if walk_dir == "": walk_dir = "any" # print(" L ", position, " ", is_walkable, " ", walk_dir, " ", required_dir) # Check if the tile is walkable and allows movement in the required direction return is_walkable and (walk_dir == required_dir or walk_dir == "any") # the first function that evaluates the finished graph # it is given a starting position and a distance in integers, which represent the amount of nodes to travel to reach the target node(s) # it will find any nodes that are the given distance away from the starting node and return them in an array # distance here is the amount of nodes to travel, not the actual distance in pixels # must use the MPNavigationNode class and the navigation_graph.get_connections(node: MPNavigationNode) function func find_nodes_with_distance(start_position: Vector2, distance: int) -> Array[MPNavigationNode]: var start_node = node_positions.get(Vector2i(start_position)) if start_node == null: print("Error: No node found at starting position ", start_position) return [] # Initialize BFS var queue: Array = [[start_node, 0]] # Each element is [node, current_distance] var visited: Dictionary = {start_node: true} var result_nodes: Array[MPNavigationNode] = [] while queue.size() > 0: var current = queue.pop_front() var current_node: MPNavigationNode = current[0] var current_distance: int = current[1] # If the target distance is reached, add the node to the result if current_distance == distance: result_nodes.append(current_node) continue # Do not explore further from this node # If the current distance exceeds the target, stop exploring if current_distance > distance: break # Explore neighbors of the current node var neighbors: Array[MPNavigationNode] = navigation_graph.get_connections(current_node) for neighbor in neighbors: if not visited.has(neighbor): visited[neighbor] = true queue.append([neighbor, current_distance + 1]) return result_nodes