extends RefCounted class_name ChessMapGenerator # Room type enum enum RoomType { STARTING, NORMAL, BOSS, FINAL, SHOP, EVENT } # Configuration var min_levels = 6 var max_levels = 12 var min_nodes_per_level = 2 var max_nodes_per_level = 4 var positions_per_level = 7 # How many horizontal positions are available (0-6) var starting_elo = 1000 var final_elo = 2100 # Internal variables var _rng = RandomNumberGenerator.new() var _next_id = 0 func _init(seed_value = null): # Set seed for reproducible maps if needed if seed_value != null: _rng.seed = seed_value else: _rng.randomize() # Main function to generate the map func generate_map(): var nodes = [] var connections = [] _next_id = 0 # Determine the number of levels var num_levels = _rng.randi_range(min_levels, max_levels) # Calculate ELO for each level var elo_step = float(final_elo - starting_elo) / (num_levels - 1) # Create starting node (always at position 3, level 0) var start_node = { "id": _get_next_id(), "type": RoomType.STARTING, "level": 0, "position": Vector2(3, 0), "elo": starting_elo } nodes.append(start_node) # Create final boss node (always at position 3, highest level) var final_node = { "id": _get_next_id(), "type": RoomType.FINAL, "level": num_levels - 1, "position": Vector2(3, num_levels - 1), "elo": final_elo } nodes.append(final_node) # Generate intermediate levels var levels_nodes = {0: [start_node], (num_levels - 1): [final_node]} for level in range(1, num_levels - 1): var level_nodes = [] # Calculate ELO for this level var level_elo = starting_elo + (elo_step * level) # Determine number of nodes for this level var num_nodes = _rng.randi_range(min_nodes_per_level, max_nodes_per_level) # Generate available positions and shuffle them var available_positions = [] for pos in range(positions_per_level): available_positions.append(pos) available_positions.shuffle() # Create nodes for this level for i in range(num_nodes): var node_type = _get_random_room_type(level, num_levels) var node = { "id": _get_next_id(), "type": node_type, "level": level, "position": Vector2(available_positions[i], level), "elo": level_elo } nodes.append(node) level_nodes.append(node) levels_nodes[level] = level_nodes # Connect nodes between levels # First connect starting node to level 1 if levels_nodes.has(1) and levels_nodes[1].size() > 0: var num_connections = min(_rng.randi_range(2, 3), levels_nodes[1].size()) var targets = levels_nodes[1].duplicate() targets.shuffle() for i in range(num_connections): connections.append({ "from": start_node.id, "to": targets[i].id }) # Keep track of which nodes are connected var connected_nodes = [start_node.id] for level in range(1, num_levels - 1): if not levels_nodes.has(level) or not levels_nodes.has(level + 1): continue var current_level_nodes = levels_nodes[level] var next_level_nodes = levels_nodes[level + 1].duplicate() next_level_nodes.shuffle() # For each node in current level that is connected from previous level for node in current_level_nodes: if _is_node_connected_to(node.id, connections): # Add to connected nodes connected_nodes.append(node.id) # Connect to 1-2 nodes in next level if not the final level if level < num_levels - 2: var num_next_connections = _rng.randi_range(1, 2) num_next_connections = min(num_next_connections, next_level_nodes.size()) for i in range(num_next_connections): if i < next_level_nodes.size(): connections.append({ "from": node.id, "to": next_level_nodes[i].id }) # Remove the selected nodes so they aren't picked again for i in range(num_next_connections): if next_level_nodes.size() > 0: next_level_nodes.pop_front() # Connect to final boss if at the level before elif level == num_levels - 2: connections.append({ "from": node.id, "to": final_node.id }) # Remove any nodes that aren't connected var valid_nodes = [] for node in nodes: if connected_nodes.has(node.id) or node.id == final_node.id: valid_nodes.append(node) # Clean up connections to removed nodes var valid_connections = [] for conn in connections: var from_valid = false var to_valid = false for node in valid_nodes: if node.id == conn.from: from_valid = true if node.id == conn.to: to_valid = true if from_valid and to_valid: valid_connections.append(conn) valid_connections = ensure_path_exists(valid_nodes, valid_connections) # Then filter out overlapping connections while maintaining connectivity valid_connections = filter_overlapping_connections(valid_nodes, valid_connections) # Perform a final check to make sure all nodes are connected var reachable_nodes = {} reachable_nodes[0] = true # Start node id is always 0 # Build the graph again var graph = {} for node in valid_nodes: graph[node.id] = [] for conn in valid_connections: if not graph.has(conn.from): graph[conn.from] = [] graph[conn.from].append(conn.to) # Do a BFS from the start node var queue = [0] # Starting node ID while queue.size() > 0: var current = queue.pop_front() if graph.has(current): for neighbor in graph[current]: if not reachable_nodes.has(neighbor): reachable_nodes[neighbor] = true queue.append(neighbor) # Check if all nodes are reachable var all_reachable = true for node in valid_nodes: if not reachable_nodes.has(node.id): all_reachable = false print("Node not reachable: ", node.id, " - Type: ", node.type) break # If not all nodes are reachable, only keep the reachable ones if not all_reachable: var truly_valid_nodes = [] for node in valid_nodes: if reachable_nodes.has(node.id): truly_valid_nodes.append(node) # Update valid_nodes to only include reachable nodes valid_nodes = truly_valid_nodes # Also update connections to only include those between reachable nodes var truly_valid_connections = [] for conn in valid_connections: if reachable_nodes.has(conn.from) and reachable_nodes.has(conn.to): truly_valid_connections.append(conn) valid_connections = truly_valid_connections space_nodes_horizontally(valid_nodes, valid_connections) # Return the generated map return { "nodes": valid_nodes, "connections": valid_connections, "levels": num_levels, "seed": _rng.seed } # Get a node type based on level - more shops and events in middle levels func _get_random_room_type(level, total_levels): # Chance of special rooms based on level var boss_chance = 0.1 + (level / float(total_levels) * 0.1) # Higher chance in later levels var shop_chance = 0.1 + (level / float(total_levels) * 0.05) # Higher chance in later levels var event_chance = 0.05 # Last non-final level should have more bosses if level == total_levels - 2: boss_chance += 0.3 var roll = _rng.randf() print("_get_random_room_type ", roll, " ", boss_chance, " ", shop_chance, " ", event_chance) if roll < boss_chance: return RoomType.BOSS elif roll < boss_chance + shop_chance: return RoomType.SHOP elif roll < boss_chance + shop_chance + event_chance: return RoomType.EVENT else: return RoomType.NORMAL # Check if a node is connected as a destination in any connection func _is_node_connected_to(node_id, connections): for conn in connections: if conn.to == node_id: return true return false # Get a unique ID for a new node func _get_next_id(): var id = _next_id _next_id += 1 return id func filter_overlapping_connections(nodes, connections): var valid_connections = [] var node_positions = {} # Create a lookup for node positions for node in nodes: node_positions[node.id] = Vector2(node.position.x, node.position.y) # Create a graph representation to check connectivity var graph = {} for node in nodes: graph[node.id] = [] for connection in connections: if not graph.has(connection.from): graph[connection.from] = [] if not graph.has(connection.to): graph[connection.to] = [] graph[connection.from].append(connection.to) # Find start and end nodes var start_node_id = -1 var end_node_id = -1 for node in nodes: if node.type == RoomType.STARTING: start_node_id = node.id elif node.type == RoomType.FINAL: end_node_id = node.id # Sort connections by priority, giving preference to: # 1. Critical path connections # 2. More vertical connections (to reduce crossing) # 3. Shorter connections var sorted_connections = connections.duplicate() sorted_connections.sort_custom(func(a, b): var a_from_pos = node_positions[a.from] var a_to_pos = node_positions[a.to] var b_from_pos = node_positions[b.from] var b_to_pos = node_positions[b.to] # Critical path connections (ones needed to reach the final boss) get highest priority var a_critical = is_on_critical_path(a.from, a.to, start_node_id, end_node_id, graph) var b_critical = is_on_critical_path(b.from, b.to, start_node_id, end_node_id, graph) if a_critical and not b_critical: return true elif not a_critical and b_critical: return false # Connections from the same node should be spread out by x-position if a.from == b.from: var a_x_diff = abs(a_from_pos.x - a_to_pos.x) var b_x_diff = abs(b_from_pos.x - b_to_pos.x) # Prefer more aligned connections (less horizontal distance) return a_x_diff < b_x_diff # Calculate how vertical each connection is var a_dx = abs(a_to_pos.x - a_from_pos.x) var a_dy = abs(a_to_pos.y - a_from_pos.y) var b_dx = abs(b_to_pos.x - b_from_pos.x) var b_dy = abs(b_to_pos.y - b_from_pos.y) var a_vertical_ratio = a_dy / max(a_dx, 0.1) # Avoid division by zero var b_vertical_ratio = b_dy / max(b_dx, 0.1) # Prefer more vertical connections if a_vertical_ratio > b_vertical_ratio * 1.5: # Threshold to make the difference significant return true elif b_vertical_ratio > a_vertical_ratio * 1.5: return false # Then prioritize by level progression (forward progress) var a_forward = a_from_pos.y < a_to_pos.y var b_forward = b_from_pos.y < b_to_pos.y if a_forward and not b_forward: return true elif not a_forward and b_forward: return false # Finally by length var a_dist = (a_to_pos - a_from_pos).length_squared() var b_dist = (b_to_pos - b_from_pos).length_squared() return a_dist < b_dist ) # First pass: Add all critical path connections for connection in sorted_connections: if is_on_critical_path(connection.from, connection.to, start_node_id, end_node_id, graph): valid_connections.append(connection) # Create a set of connected nodes from the critical path var connected_nodes = {} connected_nodes[start_node_id] = true for connection in valid_connections: connected_nodes[connection.from] = true connected_nodes[connection.to] = true # Second pass: Add remaining connections, avoiding overlaps for connection in sorted_connections: # Skip if already added if connection in valid_connections: continue # Only consider connections that link to already connected nodes if not connected_nodes.has(connection.from) and not connected_nodes.has(connection.to): continue var from_pos = node_positions[connection.from] var to_pos = node_positions[connection.to] # Check if this connection would intersect with any valid connection var has_intersection = false for valid_conn in valid_connections: var valid_from_pos = node_positions[valid_conn.from] var valid_to_pos = node_positions[valid_conn.to] # Skip if they share a node if connection.from == valid_conn.from or connection.from == valid_conn.to or \ connection.to == valid_conn.from or connection.to == valid_conn.to: continue # Check intersection if do_lines_intersect(from_pos, to_pos, valid_from_pos, valid_to_pos): has_intersection = true break # If no intersections, add to valid connections if not has_intersection: valid_connections.append(connection) connected_nodes[connection.from] = true connected_nodes[connection.to] = true # Final pass: Ensure all nodes are reachable # This part can remain the same as your current implementation return valid_connections # Helper function to check if a connection is on a critical path func is_on_critical_path(from_id, to_id, start_id, end_id, graph): # Skip the check if we're directly connecting start to end if (from_id == start_id and to_id == end_id): return true # Use breadth-first search to find paths var visited = {from_id: true} var queue = [from_id] # First check if we can reach end_id from to_id while queue.size() > 0: var current = queue.pop_front() if current == end_id: return true for neighbor in graph[current]: if not visited.has(neighbor): visited[neighbor] = true queue.append(neighbor) # If we're not on a path to the end, this isn't critical return false # Helper function to ensure at least one valid path exists from start to end func ensure_path_exists(nodes, connections): # Find start and end node IDs var start_id = -1 var end_id = -1 for node in nodes: if node.type == RoomType.STARTING: start_id = node.id elif node.type == RoomType.FINAL: end_id = node.id # Build a graph representation var graph = {} for node in nodes: graph[node.id] = [] for conn in connections: if not graph.has(conn.from): graph[conn.from] = [] graph[conn.from].append(conn.to) # Use BFS to check if end is reachable from start var visited = {start_id: true} var queue = [start_id] var path_exists = false while queue.size() > 0: var current = queue.pop_front() if current == end_id: path_exists = true # Path exists break for neighbor in graph[current]: if not visited.has(neighbor): visited[neighbor] = true queue.append(neighbor) # If path exists, return the existing connections if path_exists: return connections # Return the unmodified connections array # No path exists, need to add one # Find nodes by level var nodes_by_level = {} for node in nodes: if not nodes_by_level.has(node.level): nodes_by_level[node.level] = [] nodes_by_level[node.level].append(node) # Create a direct path from start to end through each level var current_id = start_id var levels = nodes_by_level.keys() levels.sort() # Copy connections array to avoid modifying the original var new_connections = connections.duplicate() for i in range(1, levels.size()): var level = levels[i] if nodes_by_level.has(level) and nodes_by_level[level].size() > 0: # Pick the middle node in this level var target_node = nodes_by_level[level][nodes_by_level[level].size() / 2] # Add a connection from current to this node new_connections.append({ "from": current_id, "to": target_node.id }) current_id = target_node.id # If we've reached the final level, connect to end if level == levels[-1]: new_connections.append({ "from": current_id, "to": end_id }) return new_connections func do_lines_intersect(a_start: Vector2, a_end: Vector2, b_start: Vector2, b_end: Vector2) -> bool: # Line segment intersection using the parametric equation approach var s1_x = a_end.x - a_start.x var s1_y = a_end.y - a_start.y var s2_x = b_end.x - b_start.x var s2_y = b_end.y - b_start.y var denominator = (-s2_x * s1_y + s1_x * s2_y) if denominator == 0: return false # Collinear or parallel lines var s = (-s1_y * (a_start.x - b_start.x) + s1_x * (a_start.y - b_start.y)) / denominator var t = (s2_x * (a_start.y - b_start.y) - s2_y * (a_start.x - b_start.x)) / denominator # If s and t are between 0-1, the lines intersect return (s >= 0 && s <= 1 && t >= 0 && t <= 1) # Fixed version of space_nodes_horizontally with proper connection tracking func space_nodes_horizontally(nodes, connections): # Group nodes by level var nodes_by_level = {} for node in nodes: if not nodes_by_level.has(node.level): nodes_by_level[node.level] = [] nodes_by_level[node.level].append(node) # Process each level to spread out nodes for level in nodes_by_level.keys(): var level_nodes = nodes_by_level[level] # Skip levels with just one node if level_nodes.size() <= 1: continue # Sort nodes by their current horizontal position level_nodes.sort_custom(func(a, b): return a.position.x < b.position.x) # First pass: calculate connections for each node var node_connections = {} for node in level_nodes: node_connections[node.id] = { "above": [], "below": [] } # Find connections to this node for conn in connections: # Connection from above if conn.to == node.id: var from_node = null for n in nodes: if n.id == conn.from and n.level < node.level: from_node = n break if from_node: node_connections[node.id].above.append(from_node) # Connection to below elif conn.from == node.id: var to_node = null for n in nodes: if n.id == conn.to and n.level > node.level: to_node = n break if to_node: node_connections[node.id].below.append(to_node) # Second pass: adjust node positions based on connections for node in level_nodes: # Get this node's connections var connections_above = node_connections[node.id].above var connections_below = node_connections[node.id].below # Calculate "pull" forces from connected nodes var pull_x = 0.0 var total_pull = 0.0 # Look at connections to nodes above for conn_node in connections_above: pull_x += conn_node.position.x total_pull += 1.0 # Look at connections to nodes below for conn_node in connections_below: pull_x += conn_node.position.x total_pull += 1.0 # Adjust position based on pull if there are connections if total_pull > 0: # Calculate ideal x position (average of connected nodes) var ideal_x = pull_x / total_pull # Move slightly toward ideal position (don't move fully to maintain ordering) node.position.x = node.position.x * 0.7 + ideal_x * 0.3 # Ensure minimum spacing between nodes var min_spacing = 1.0 # Minimum horizontal distance between nodes for i in range(1, level_nodes.size()): var prev_node = level_nodes[i-1] var curr_node = level_nodes[i] # Check if nodes are too close if curr_node.position.x - prev_node.position.x < min_spacing: # Shift this node and all subsequent nodes to the right var shift = min_spacing - (curr_node.position.x - prev_node.position.x) for j in range(i, level_nodes.size()): level_nodes[j].position.x += shift return nodes