630 lines
22 KiB
GDScript
630 lines
22 KiB
GDScript
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
|