ChessBuilder/Systems/Game/Map/NodeLayoutHelper.gd
2025-03-04 11:03:28 -06:00

147 lines
No EOL
5.2 KiB
GDScript

extends RefCounted
class_name NodeLayoutHelper
# Configuration
var map_width: int = 6 # Available horizontal positions (0-5)
var level_height: float = 1.0 # Vertical spacing between levels
var node_spread: float = 0.7 # How much to spread nodes horizontally (0-1)
var path_smoothness: float = 0.3 # How much to align nodes with their connections (0-1)
# Internal
var _rng = RandomNumberGenerator.new()
func _init(seed_value: int = 0):
if seed_value != 0:
_rng.seed = seed_value
else:
_rng.randomize()
# Organize node positions for better visual layout
func organize_layout(nodes: Array, connections: Array) -> Array:
# Clone the nodes so we don't modify the original array
var updated_nodes = []
for node in nodes:
updated_nodes.append(node.duplicate())
# Group nodes by level
var nodes_by_level = {}
for node in updated_nodes:
var level = node.level
if not nodes_by_level.has(level):
nodes_by_level[level] = []
nodes_by_level[level].append(node)
# Process each level
for level in nodes_by_level:
var level_nodes = nodes_by_level[level]
# Skip levels with fixed positions
if level == 0 or level == nodes_by_level.size() - 1:
continue
# Calculate optimal positions based on connections
for node in level_nodes:
_adjust_node_position(node, updated_nodes, connections, nodes_by_level)
# Resolve overlaps after initial placement
for level in nodes_by_level:
var level_nodes = nodes_by_level[level]
if level_nodes.size() > 1:
_resolve_horizontal_overlaps(level_nodes)
return updated_nodes
# Adjust node position based on its connections
func _adjust_node_position(node, all_nodes, connections, nodes_by_level):
# Find all connections to/from this node
var connected_from = [] # Nodes in the previous level connecting to this node
var connected_to = [] # Nodes in the next level this node connects to
for conn in connections:
if conn.to == node.id:
var from_node = _find_node_by_id(all_nodes, conn.from)
if from_node and from_node.level == node.level - 1:
connected_from.append(from_node)
if conn.from == node.id:
var to_node = _find_node_by_id(all_nodes, conn.to)
if to_node and to_node.level == node.level + 1:
connected_to.append(to_node)
# Calculate optimal x position based on connected nodes
var optimal_x = node.position.x # Start with current position
if connected_from.size() > 0 or connected_to.size() > 0:
var avg_x = 0.0
var total_connected = 0
# Consider positions of connected nodes
for from_node in connected_from:
avg_x += from_node.position.x
total_connected += 1
for to_node in connected_to:
avg_x += to_node.position.x
total_connected += 1
if total_connected > 0:
avg_x /= total_connected
# Blend between current position and optimal position
optimal_x = lerp(node.position.x, avg_x, path_smoothness)
# Add some random variation to prevent everything aligning too perfectly
optimal_x += (_rng.randf() - 0.5) * node_spread
# Keep within bounds
optimal_x = clamp(optimal_x, 0, map_width)
# Update node position
node.position.x = optimal_x
# Y position is determined by level
node.position.y = node.level * level_height
# Resolve horizontal overlaps between nodes on the same level
func _resolve_horizontal_overlaps(level_nodes):
# Sort nodes by x position
level_nodes.sort_custom(func(a, b): return a.position.x < b.position.x)
# Minimum distance between nodes
var min_distance = 1.0
# Check for overlaps and adjust
for i in range(1, level_nodes.size()):
var prev_node = level_nodes[i-1]
var curr_node = level_nodes[i]
if curr_node.position.x - prev_node.position.x < min_distance:
# If too close, move them apart
var midpoint = (prev_node.position.x + curr_node.position.x) / 2
var half_distance = min_distance / 2
# Try to maintain relative positions
prev_node.position.x = midpoint - half_distance
curr_node.position.x = midpoint + half_distance
# Ensure we stay within bounds
if prev_node.position.x < 0:
var shift = -prev_node.position.x
prev_node.position.x += shift
curr_node.position.x += shift
if curr_node.position.x > map_width:
var shift = curr_node.position.x - map_width
prev_node.position.x -= shift
curr_node.position.x -= shift
# Final boundary check for previous node
prev_node.position.x = max(0, prev_node.position.x)
# Find a node by its ID
func _find_node_by_id(nodes, id):
for node in nodes:
if node.id == id:
return node
return null