537 lines
18 KiB
GDScript
537 lines
18 KiB
GDScript
extends Control
|
|
class_name MapScreen
|
|
|
|
signal back_pressed
|
|
signal node_selected(node_data)
|
|
|
|
const MapGenerator = preload("res://Systems/Game/Map/MapGenerator.gd")
|
|
# Room type constants
|
|
enum RoomType {
|
|
STARTING,
|
|
NORMAL,
|
|
BOSS,
|
|
FINAL,
|
|
SHOP,
|
|
EVENT
|
|
}
|
|
|
|
# Node config
|
|
const NODE_SIZE = Vector2(60, 60)
|
|
const NODE_SPACING_X = 150
|
|
const NODE_SPACING_Y = 120
|
|
const LINE_WIDTH = 3
|
|
const LINE_COLOR = Color(0.2, 0.2, 0.2)
|
|
const LINE_COLOR_CRIT = Color(0.9, 0.8, 0.2, 0.8)
|
|
const LINE_COLOR_SELECTED = Color(0.2, 0.6, 0.2)
|
|
const LINE_COLOR_ACCESSIBLE = Color(0.6, 0.6, 0.2)
|
|
|
|
# Node symbols and colors
|
|
const NODE_SYMBOLS = {
|
|
RoomType.STARTING: "O", # Circle
|
|
RoomType.NORMAL: "■", # Square
|
|
RoomType.BOSS: "★", # Star
|
|
RoomType.FINAL: "✱", # Burst
|
|
RoomType.SHOP: "₵", # Star
|
|
RoomType.EVENT: "?" # Burst
|
|
}
|
|
|
|
const NODE_COLORS = {
|
|
RoomType.STARTING: Color(1, 1, 1), # White
|
|
RoomType.NORMAL: Color(0.2, 0.2, 0.2), # Dark gray
|
|
RoomType.BOSS: Color(0.9, 0.2, 0.2), # Red
|
|
RoomType.FINAL: Color(0.9, 0.9, 0.2), # Yellow
|
|
RoomType.SHOP: Color(0.2, 0.7, 0.9), # Yellow
|
|
RoomType.EVENT: Color(0.8, 0.4, 0.9) # Yellow
|
|
}
|
|
|
|
var map_nodes = []
|
|
var map_connections = []
|
|
var node_buttons = {}
|
|
var connection_lines = []
|
|
var traversed_map = []
|
|
var current_node = null
|
|
var levels = 5
|
|
|
|
const SCROLL_PADDING_TOP = 80
|
|
const SCROLL_PADDING_BOTTOM = 80
|
|
const SCROLL_PADDING_LEFT = 60
|
|
const SCROLL_PADDING_RIGHT = 60
|
|
|
|
|
|
@onready var map_container = $MapScrollContainer/MapContainer
|
|
@onready var back_button = $BackButton
|
|
@onready var title_label = $TitleLabel
|
|
@onready var legend_container = $LegendContainer
|
|
|
|
func _ready():
|
|
# Connect back button
|
|
if back_button:
|
|
back_button.connect("pressed", Callable(self, "_on_back_button_pressed"))
|
|
|
|
# Create legend
|
|
create_legend()
|
|
|
|
# Generate default map
|
|
generate_map()
|
|
|
|
# Display the map
|
|
display_map()
|
|
|
|
func create_legend():
|
|
# Create legend for room types
|
|
var legend_items = [
|
|
{"type": RoomType.BOSS, "text": "Boss"},
|
|
{"type": RoomType.STARTING, "text": "Starting Room"},
|
|
{"type": RoomType.FINAL, "text": "Final Boss"},
|
|
{"type": RoomType.NORMAL, "text": "Normal Room"}
|
|
]
|
|
|
|
for item in legend_items:
|
|
var hbox = HBoxContainer.new()
|
|
hbox.add_theme_constant_override("separation", 10)
|
|
|
|
var symbol = Label.new()
|
|
symbol.text = NODE_SYMBOLS[item.type]
|
|
symbol.add_theme_color_override("font_color", NODE_COLORS[item.type])
|
|
symbol.add_theme_font_size_override("font_size", 24)
|
|
|
|
var text = Label.new()
|
|
text.text = item.text
|
|
|
|
hbox.add_child(symbol)
|
|
hbox.add_child(text)
|
|
legend_container.add_child(hbox)
|
|
|
|
func generate_map():
|
|
# Clear existing map
|
|
map_nodes.clear()
|
|
map_connections.clear()
|
|
var mapGen = MapGenerator.new().generate_map()
|
|
print("MapGen ", mapGen)
|
|
# Create starting node
|
|
# var start_node = {
|
|
# "id": 0,
|
|
# "type": RoomType.STARTING,
|
|
# "level": 0,
|
|
# "position": Vector2(3, 4) # Column, Row
|
|
# }
|
|
# map_nodes.append(start_node)
|
|
# current_node = start_node
|
|
|
|
# # Create final boss node
|
|
# var final_node = {
|
|
# "id": 1,
|
|
# "type": RoomType.FINAL,
|
|
# "level": levels - 1,
|
|
# "position": Vector2(3, 0) # Column, Row
|
|
# }
|
|
# map_nodes.append(final_node)
|
|
for node_data in mapGen.nodes:
|
|
add_node(
|
|
node_data.id,
|
|
node_data.type,
|
|
node_data.level,
|
|
node_data.position,
|
|
node_data.elo
|
|
)
|
|
|
|
# Store elo rating if needed
|
|
map_nodes[map_nodes.size() - 1].elo = node_data.elo
|
|
|
|
# Set current node to starting node (id 0)
|
|
if node_data.id == 0:
|
|
current_node = map_nodes[map_nodes.size() - 1]
|
|
|
|
# Process connections
|
|
for connection in mapGen.connections:
|
|
add_connection(connection.from, connection.to)
|
|
|
|
|
|
func add_node(id, type, level, position, elo):
|
|
map_nodes.append({
|
|
"id": id,
|
|
"type": type,
|
|
"level": level,
|
|
"position": position,
|
|
"elo": elo
|
|
})
|
|
|
|
func add_connection(from_id, to_id):
|
|
map_connections.append({
|
|
"from": from_id,
|
|
"to": to_id
|
|
})
|
|
|
|
func display_map():
|
|
# Clear previous display
|
|
for child in map_container.get_children():
|
|
child.queue_free()
|
|
node_buttons.clear()
|
|
connection_lines = []
|
|
|
|
# Calculate map dimensions for proper padding
|
|
var min_y = 9999
|
|
var max_y = 0
|
|
var min_x = 9999
|
|
var max_x = 0
|
|
|
|
# Find boundaries of the map
|
|
for node_data in map_nodes:
|
|
var x_pos = node_data.position.x * NODE_SPACING_X
|
|
var y_pos = node_data.position.y * NODE_SPACING_Y
|
|
min_x = min(min_x, x_pos)
|
|
max_x = max(max_x, x_pos)
|
|
min_y = min(min_y, y_pos)
|
|
max_y = max(max_y, y_pos)
|
|
|
|
# Add padding to container size
|
|
var map_width = max_x - min_x + NODE_SIZE.x * 2 + SCROLL_PADDING_LEFT + SCROLL_PADDING_RIGHT
|
|
var map_height = max_y - min_y + NODE_SIZE.y * 2 + SCROLL_PADDING_TOP + SCROLL_PADDING_BOTTOM
|
|
map_container.custom_minimum_size = Vector2(map_width, map_height)
|
|
|
|
# Create padding nodes
|
|
# Create a padding node at the top
|
|
var top_padding = Control.new()
|
|
top_padding.custom_minimum_size = Vector2(map_width, SCROLL_PADDING_TOP)
|
|
top_padding.position = Vector2(0, 0)
|
|
map_container.add_child(top_padding)
|
|
|
|
# Create a padding node at the bottom
|
|
var bottom_padding = Control.new()
|
|
bottom_padding.custom_minimum_size = Vector2(map_width, SCROLL_PADDING_BOTTOM)
|
|
bottom_padding.position = Vector2(0, map_height - SCROLL_PADDING_BOTTOM)
|
|
map_container.add_child(bottom_padding)
|
|
|
|
# Create padding nodes for left and right
|
|
var left_padding = Control.new()
|
|
left_padding.custom_minimum_size = Vector2(SCROLL_PADDING_LEFT, map_height)
|
|
left_padding.position = Vector2(0, 0)
|
|
map_container.add_child(left_padding)
|
|
|
|
var right_padding = Control.new()
|
|
right_padding.custom_minimum_size = Vector2(SCROLL_PADDING_RIGHT, map_height)
|
|
right_padding.position = Vector2(map_width - SCROLL_PADDING_RIGHT, 0)
|
|
map_container.add_child(right_padding)
|
|
|
|
# Find the optimal path from start to end for highlighting
|
|
var critical_path = find_unique_path_to_end()
|
|
var critical_connections = []
|
|
|
|
# Convert path nodes to connections
|
|
if critical_path.size() > 1:
|
|
for i in range(critical_path.size() - 1):
|
|
critical_connections.append({
|
|
"from": critical_path[i],
|
|
"to": critical_path[i + 1]
|
|
})
|
|
|
|
# First draw non-critical connections (so critical ones are on top)
|
|
for connection in map_connections:
|
|
var is_critical = false
|
|
for crit_conn in critical_connections:
|
|
if connection.from == crit_conn.from and connection.to == crit_conn.to:
|
|
is_critical = true
|
|
break
|
|
|
|
if not is_critical:
|
|
var from_node = get_node_by_id(connection.from)
|
|
var to_node = get_node_by_id(connection.to)
|
|
|
|
if from_node and to_node:
|
|
draw_curved_connection(from_node, to_node, false)
|
|
|
|
# Then draw critical connections on top
|
|
for crit_conn in critical_connections:
|
|
var from_node = get_node_by_id(crit_conn.from)
|
|
var to_node = get_node_by_id(crit_conn.to)
|
|
|
|
if from_node and to_node:
|
|
draw_curved_connection(from_node, to_node, true)
|
|
|
|
|
|
# Draw nodes
|
|
for node_data in map_nodes:
|
|
draw_node(node_data)
|
|
|
|
func get_node_by_id(id):
|
|
for node in map_nodes:
|
|
if node.id == id:
|
|
return node
|
|
return null
|
|
|
|
func draw_node(node_data):
|
|
var button = Button.new()
|
|
button.text = NODE_SYMBOLS[node_data.type]
|
|
button.custom_minimum_size = NODE_SIZE
|
|
button.flat = true
|
|
|
|
# Add some styling
|
|
var font = FontFile.new()
|
|
var style = StyleBoxFlat.new()
|
|
style.bg_color = Color(0.1, 0.1, 0.1, 0.5)
|
|
style.corner_radius_top_left = 30
|
|
style.corner_radius_top_right = 30
|
|
style.corner_radius_bottom_left = 30
|
|
style.corner_radius_bottom_right = 30
|
|
style.border_width_left = 2
|
|
style.border_width_top = 2
|
|
style.border_width_right = 2
|
|
style.border_width_bottom = 2
|
|
style.border_color = NODE_COLORS[node_data.type]
|
|
|
|
button.add_theme_font_size_override("font_size", 28)
|
|
button.add_theme_color_override("font_color", NODE_COLORS[node_data.type])
|
|
button.add_theme_stylebox_override("normal", style)
|
|
|
|
# Position the node with top and left padding adjustments
|
|
var x_pos = node_data.position.x * NODE_SPACING_X + SCROLL_PADDING_LEFT
|
|
var y_pos = node_data.position.y * NODE_SPACING_Y + SCROLL_PADDING_TOP
|
|
button.position = Vector2(x_pos, y_pos) - NODE_SIZE/2
|
|
|
|
# Store data
|
|
button.set_meta("node_data", node_data)
|
|
|
|
# Connect signal
|
|
button.pressed.connect(func(): _on_node_pressed(node_data))
|
|
|
|
# Add to map
|
|
map_container.add_child(button)
|
|
node_buttons[node_data.id] = button
|
|
|
|
# Highlight current node
|
|
if current_node and node_data.id == current_node.id:
|
|
highlight_current_node(button)
|
|
|
|
func draw_connection(from_node, to_node):
|
|
var line = Line2D.new()
|
|
line.width = LINE_WIDTH
|
|
line.default_color = LINE_COLOR
|
|
if from_node.id == current_node.id:
|
|
line.default_color = LINE_COLOR_ACCESSIBLE
|
|
elif traversed_map.has(to_node.id) and (traversed_map.has(from_node.id) || from_node.id == 0):
|
|
line.default_color = LINE_COLOR_SELECTED
|
|
|
|
# Calculate start and end positions with top and left padding adjustments
|
|
var start_pos = from_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP)
|
|
var end_pos = to_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP)
|
|
|
|
# Add points
|
|
line.add_point(start_pos)
|
|
line.add_point(end_pos)
|
|
|
|
# Add to map
|
|
map_container.add_child(line)
|
|
connection_lines.append(line)
|
|
|
|
func highlight_current_node(button):
|
|
var style = button.get_theme_stylebox("normal").duplicate()
|
|
style.bg_color = Color(0.3, 0.3, 0.3, 0.7)
|
|
style.border_width_left = 4
|
|
style.border_width_top = 4
|
|
style.border_width_right = 4
|
|
style.border_width_bottom = 4
|
|
button.add_theme_stylebox_override("normal", style)
|
|
|
|
func _on_node_pressed(node_data):
|
|
# Check if the node is accessible from current node
|
|
if is_node_accessible(node_data):
|
|
# Update current node
|
|
current_node = node_data
|
|
traversed_node(node_data)
|
|
# Refresh display to update highlights
|
|
display_map()
|
|
|
|
# Emit signal with selected node data
|
|
emit_signal("node_selected", node_data)
|
|
else:
|
|
print("Node not accessible from current position", node_data)
|
|
|
|
func traversed_node(node_data):
|
|
traversed_map.append(node_data.id)
|
|
|
|
func is_node_accessible(node_data):
|
|
# If no current node, only starting node is accessible
|
|
if current_node == null:
|
|
return node_data.type == RoomType.STARTING
|
|
|
|
# Check if there's a direct connection from current node to target node
|
|
for connection in map_connections:
|
|
if connection.from == current_node.id and connection.to == node_data.id:
|
|
return true
|
|
|
|
return false
|
|
|
|
func _on_back_button_pressed():
|
|
emit_signal("back_pressed")
|
|
visible = false
|
|
|
|
func draw_curved_connection(from_node, to_node, isCritPath):
|
|
var line = Line2D.new()
|
|
line.width = LINE_WIDTH
|
|
line.default_color = LINE_COLOR
|
|
# if isCritPath:
|
|
# line.default_color = LINE_COLOR_CRIT
|
|
if from_node.id == current_node.id:
|
|
line.default_color = LINE_COLOR_ACCESSIBLE
|
|
elif traversed_map.has(to_node.id) and (traversed_map.has(from_node.id) || from_node.id == 0):
|
|
line.default_color = LINE_COLOR_SELECTED
|
|
|
|
# Calculate start and end positions with padding adjustments
|
|
var start_pos = from_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP)
|
|
var end_pos = to_node.position * Vector2(NODE_SPACING_X, NODE_SPACING_Y) + Vector2(SCROLL_PADDING_LEFT, SCROLL_PADDING_TOP)
|
|
|
|
# Find all connections from the same starting node to help with path separation
|
|
var sibling_connections = []
|
|
for conn in map_connections:
|
|
if conn.from == from_node.id and conn.to != to_node.id:
|
|
var other_node = get_node_by_id(conn.to)
|
|
if other_node:
|
|
sibling_connections.append(other_node)
|
|
|
|
# Count how many connections this source node has
|
|
var connection_count = sibling_connections.size() + 1 # +1 for this connection
|
|
|
|
# Determine position of this connection among siblings (for consistent curve offsets)
|
|
var connection_index = 0
|
|
for i in range(sibling_connections.size()):
|
|
if sibling_connections[i].id < to_node.id:
|
|
connection_index += 1
|
|
|
|
# Create a bezier curve path
|
|
var points = []
|
|
|
|
# Calculate direction vector between nodes
|
|
var direction = (end_pos - start_pos).normalized()
|
|
var perp_direction = Vector2(-direction.y, direction.x) # Perpendicular to direction
|
|
|
|
# Different curve handling based on node positions
|
|
if from_node.position.y != to_node.position.y:
|
|
# Vertical/diagonal connection between different levels
|
|
|
|
# Calculate curve offset based on connection count and index
|
|
var curve_offset = 0.0
|
|
if connection_count > 1:
|
|
# Calculate spacing between -0.5 and 0.5
|
|
var spacing = 1.0 / (connection_count + 1)
|
|
curve_offset = (connection_index + 1) * spacing - 0.5
|
|
curve_offset *= NODE_SPACING_X * 0.8 # Scale by node spacing
|
|
|
|
# Generate control points with horizontal offset
|
|
var dist = (end_pos - start_pos).length()
|
|
var mid_point = start_pos + (end_pos - start_pos) * 0.5
|
|
|
|
# Apply the offset perpendicular to the path direction
|
|
mid_point += perp_direction * curve_offset
|
|
|
|
# Calculate control points with vertical offsets
|
|
var control1 = start_pos + direction * (dist * 0.25) + perp_direction * curve_offset * 1.5
|
|
var control2 = end_pos - direction * (dist * 0.25) + perp_direction * curve_offset * 1.5
|
|
|
|
# Generate points for a cubic bezier curve
|
|
for i in range(21): # More points for a smoother curve
|
|
var t = i / 20.0
|
|
var point = cubic_bezier(start_pos, control1, control2, end_pos, t)
|
|
points.append(point)
|
|
else:
|
|
# Horizontal connection (nodes at same level)
|
|
var curve_height = NODE_SPACING_Y * 0.4 # Base curve height
|
|
|
|
# Adjust curve height based on connection position
|
|
if connection_count > 1:
|
|
curve_height *= (1.0 + 0.3 * connection_index)
|
|
|
|
# Determine curve direction (up or down)
|
|
var curve_up = true
|
|
|
|
# If nodes are further apart horizontally, make a higher curve
|
|
var x_distance = abs(from_node.position.x - to_node.position.x)
|
|
if x_distance > 1:
|
|
curve_height *= (1.0 + 0.2 * x_distance)
|
|
|
|
# Calculate mid point with vertical offset
|
|
var mid_x = (start_pos.x + end_pos.x) / 2
|
|
var mid_y = null
|
|
if curve_up:
|
|
mid_y = start_pos - curve_height
|
|
else :
|
|
mid_y = start_pos - (-curve_height)
|
|
var mid_point = Vector2(mid_x, mid_y)
|
|
|
|
# Generate points for a quadratic bezier curve
|
|
for i in range(21):
|
|
var t = i / 20.0
|
|
var point = quadratic_bezier(start_pos, mid_point, end_pos, t)
|
|
points.append(point)
|
|
|
|
# Apply anti-aliasing
|
|
line.antialiased = true
|
|
|
|
# Add the points to the line
|
|
for point in points:
|
|
line.add_point(point)
|
|
|
|
# Add to map
|
|
map_container.add_child(line)
|
|
connection_lines.append(line)
|
|
|
|
func find_unique_path_to_end():
|
|
# Find start and end nodes
|
|
var start_node = null
|
|
var end_node = null
|
|
|
|
for node in map_nodes:
|
|
if node.type == RoomType.STARTING:
|
|
start_node = node
|
|
elif node.type == RoomType.FINAL:
|
|
end_node = node
|
|
|
|
if not start_node or not end_node:
|
|
return []
|
|
|
|
# Build a graph representation
|
|
var graph = {}
|
|
for node in map_nodes:
|
|
graph[node.id] = []
|
|
|
|
for conn in map_connections:
|
|
if graph.has(conn.from):
|
|
graph[conn.from].append(conn.to)
|
|
|
|
# Use BFS to find the shortest path
|
|
var queue = [[start_node.id]] # Queue of paths
|
|
var visited = {start_node.id: true}
|
|
|
|
while queue.size() > 0:
|
|
var path = queue.pop_front()
|
|
var node_id = path[path.size() - 1]
|
|
|
|
if node_id == end_node.id:
|
|
return path # Return the full path
|
|
|
|
if graph.has(node_id):
|
|
for neighbor in graph[node_id]:
|
|
if not visited.has(neighbor):
|
|
visited[neighbor] = true
|
|
var new_path = path.duplicate()
|
|
new_path.append(neighbor)
|
|
queue.append(new_path)
|
|
|
|
return [] # No path found
|
|
|
|
# Helper function for cubic bezier curve calculation
|
|
func cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float) -> Vector2:
|
|
var q0 = p0.lerp(p1, t)
|
|
var q1 = p1.lerp(p2, t)
|
|
var q2 = p2.lerp(p3, t)
|
|
|
|
var r0 = q0.lerp(q1, t)
|
|
var r1 = q1.lerp(q2, t)
|
|
|
|
return r0.lerp(r1, t)
|
|
|
|
# Helper function for quadratic bezier curve calculation
|
|
func quadratic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, t: float) -> Vector2:
|
|
var q0 = p0.lerp(p1, t)
|
|
var q1 = p1.lerp(p2, t)
|
|
|
|
return q0.lerp(q1, t)
|