extends Control ## Slay-the-Spire-style run map shown between fights. Placeholder art — coloured pips ## joined by paths — but fully interactive: the player picks one of the reachable nodes ## and is sent into that level. Every node is the arena for now (see run_state.gd); the ## screen is level-agnostic, so richer levels just need a LevelDef with its own colour. const _GOLD := Color(1.00, 0.78, 0.22) const _CREAM := Color(0.93, 0.88, 0.72) const _EDGE_DIM := Color(0.45, 0.36, 0.18, 0.55) const _EDGE_ACTIVE := Color(1.00, 0.82, 0.32, 0.95) const _NODE_SIZE := 58.0 const _BOSS_SIZE := 88.0 const _MENU_SCENE := "res://MainMenu.tscn" var _map_area: Control var _completion: PanelContainer func _ready() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE get_tree().paused = false Run.ensure_run() _build_ui() _relayout.call_deferred() # defer so _map_area has its real size first func _build_ui() -> void: set_anchors_preset(Control.PRESET_FULL_RECT) var bg := ColorRect.new() bg.set_anchors_preset(Control.PRESET_FULL_RECT) bg.color = Color(0.09, 0.06, 0.03) # opaque leather, no scene showing through bg.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(bg) var title := Label.new() title.text = "Choose Your Path" title.set_anchors_preset(Control.PRESET_TOP_WIDE) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER title.offset_top = 22.0 title.add_theme_font_override("font", UiFonts.title()) title.add_theme_font_size_override("font_size", 46) title.add_theme_color_override("font_color", _GOLD) title.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9)) title.add_theme_constant_override("outline_size", 6) title.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(title) _map_area = Control.new() _map_area.set_anchors_preset(Control.PRESET_FULL_RECT) _map_area.offset_top = 108.0 _map_area.offset_bottom = -92.0 _map_area.offset_left = 48.0 _map_area.offset_right = -48.0 _map_area.clip_contents = false add_child(_map_area) _map_area.resized.connect(_relayout) var menu_btn := _make_button("Main Menu") menu_btn.set_anchors_preset(Control.PRESET_BOTTOM_LEFT) menu_btn.grow_vertical = Control.GROW_DIRECTION_BEGIN menu_btn.offset_left = 32.0 menu_btn.offset_bottom = -26.0 menu_btn.pressed.connect(_to_menu) add_child(menu_btn) _build_completion() # ── Layout ────────────────────────────────────────────────────────────────── # Rebuilt from the run state on first show and whenever the map area resizes. Edges are # added first so the node pips draw on top of the paths. func _relayout() -> void: if _map_area == null: return for child in _map_area.get_children(): child.queue_free() var size := _map_area.size if size.x <= 0.0 or size.y <= 0.0: return var reachable := Run.reachable() var reach_set := {} for n: MapNode in reachable: reach_set[n] = true var here := Run.current_node() if Run.player_row < 0: var start_pos := Vector2(size.x * 0.5, size.y - 8.0) _add_start_pip(start_pos) for n: MapNode in reachable: _add_edge(start_pos, _local(n, size), true) for row: Array in Run.map: for from_node: MapNode in row: for to_col: int in from_node.links: var to_node := Run.node_at(from_node.row + 1, to_col) if to_node == null: continue var active := from_node == here and reach_set.has(to_node) _add_edge(_local(from_node, size), _local(to_node, size), active) for row: Array in Run.map: for node: MapNode in row: _add_node(node, size, reach_set.has(node)) _completion.visible = Run.run_complete() or reachable.is_empty() func _local(node: MapNode, size: Vector2) -> Vector2: return Vector2(node.pos.x * size.x, node.pos.y * size.y) func _add_edge(a: Vector2, b: Vector2, active: bool) -> void: var line := Line2D.new() line.add_point(a) line.add_point(b) line.width = 4.0 if active else 2.5 line.default_color = _EDGE_ACTIVE if active else _EDGE_DIM line.antialiased = true _map_area.add_child(line) func _add_start_pip(center: Vector2) -> void: var pip := PanelContainer.new() var sz := 22.0 pip.custom_minimum_size = Vector2(sz, sz) pip.size = Vector2(sz, sz) pip.position = center - Vector2(sz, sz) * 0.5 pip.mouse_filter = Control.MOUSE_FILTER_IGNORE pip.add_theme_stylebox_override("panel", UiTheme.disc(_GOLD, _CREAM, 2)) _map_area.add_child(pip) func _add_node(node: MapNode, size: Vector2, is_reachable: bool) -> void: var is_current := node.row == Run.player_row and node.col == Run.player_col var sz := _BOSS_SIZE if node.level.is_boss else _NODE_SIZE var center := _local(node, size) var btn := Button.new() btn.custom_minimum_size = Vector2(sz, sz) btn.size = Vector2(sz, sz) btn.position = center - Vector2(sz, sz) * 0.5 btn.focus_mode = Control.FOCUS_NONE btn.disabled = not is_reachable btn.mouse_filter = Control.MOUSE_FILTER_STOP if is_reachable else Control.MOUSE_FILTER_IGNORE var fill := node.level.color var border := UiTheme.BORDER if is_current: border = _CREAM elif is_reachable: border = _GOLD elif node.cleared: fill = fill.darkened(0.35) border = Color(_GOLD.r, _GOLD.g, _GOLD.b, 0.5) else: # locked — a future row not yet reachable fill = fill.darkened(0.6) border = Color(0.40, 0.34, 0.20, 0.6) var border_w := 4 if (is_reachable or is_current) else 2 var normal := UiTheme.disc(fill, border, border_w) var hover := UiTheme.disc(fill.lightened(0.18), _CREAM, border_w) btn.add_theme_stylebox_override("normal", normal) btn.add_theme_stylebox_override("hover", hover) btn.add_theme_stylebox_override("pressed", hover) btn.add_theme_stylebox_override("disabled", normal) _map_area.add_child(btn) if is_reachable: btn.pressed.connect(_enter_node.bind(node)) _pulse(btn) if is_reachable or is_current or node.level.is_boss: _add_caption(node.level.display_name, center + Vector2(0.0, sz * 0.5 + 6.0), is_reachable) if is_current: _add_caption("You are here", center - Vector2(0.0, sz * 0.5 + 18.0), false) func _add_caption(text: String, center_top: Vector2, bright: bool) -> void: var lbl := Label.new() lbl.text = text lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER lbl.size = Vector2(200.0, 0.0) lbl.position = center_top - Vector2(100.0, 0.0) lbl.add_theme_font_override("font", UiFonts.body()) lbl.add_theme_font_size_override("font_size", 15) lbl.add_theme_color_override("font_color", _CREAM if bright else UiTheme.MUTED) lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9)) lbl.add_theme_constant_override("outline_size", 4) lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE _map_area.add_child(lbl) # Slow glow on the reachable pips so the eye lands on where the player can go next. func _pulse(node: CanvasItem) -> void: var tw := create_tween().set_loops() tw.tween_property(node, "modulate:a", 0.6, 0.75).set_trans(Tween.TRANS_SINE) tw.tween_property(node, "modulate:a", 1.0, 0.75).set_trans(Tween.TRANS_SINE) # ── Actions ─────────────────────────────────────────────────────────────────── func _enter_node(node: MapNode) -> void: Run.select_node(node) get_tree().paused = false get_tree().change_scene_to_file(node.level.scene_path) func _to_menu() -> void: get_tree().paused = false Input.mouse_mode = Input.MOUSE_MODE_VISIBLE get_tree().change_scene_to_file(_MENU_SCENE) # ── Run-complete overlay ────────────────────────────────────────────────────── func _build_completion() -> void: _completion = PanelContainer.new() _completion.set_anchors_preset(Control.PRESET_CENTER) _completion.grow_horizontal = Control.GROW_DIRECTION_BOTH _completion.grow_vertical = Control.GROW_DIRECTION_BOTH _completion.add_theme_stylebox_override( "panel", UiTheme.plaque(UiTheme.LEATHER, _GOLD, 4, 32.0, 24.0) ) _completion.visible = false add_child(_completion) var col := VBoxContainer.new() col.add_theme_constant_override("separation", 16) col.alignment = BoxContainer.ALIGNMENT_CENTER _completion.add_child(col) var heading := Label.new() heading.text = "Run Complete!" heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER heading.add_theme_font_override("font", UiFonts.title()) heading.add_theme_font_size_override("font_size", 40) heading.add_theme_color_override("font_color", _GOLD) heading.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9)) heading.add_theme_constant_override("outline_size", 5) col.add_child(heading) var row := HBoxContainer.new() row.alignment = BoxContainer.ALIGNMENT_CENTER row.add_theme_constant_override("separation", 16) col.add_child(row) var again := _make_button("New Run") again.pressed.connect(_new_run) row.add_child(again) var menu := _make_button("Main Menu") menu.pressed.connect(_to_menu) row.add_child(menu) func _new_run() -> void: Run.start_new_run() _relayout() # Shared thematic button, matching the HUD / menu plaques. func _make_button(text: String) -> Button: var btn := Button.new() btn.text = text btn.focus_mode = Control.FOCUS_NONE btn.add_theme_stylebox_override( "normal", UiTheme.plaque(Color(0.10, 0.07, 0.03, 0.90), _GOLD, 3, 18.0, 9.0) ) var hover := UiTheme.plaque(Color(0.16, 0.10, 0.04, 0.96), _CREAM, 3, 18.0, 9.0) btn.add_theme_stylebox_override("hover", hover) btn.add_theme_stylebox_override("pressed", hover) btn.add_theme_font_override("font", UiFonts.body()) btn.add_theme_color_override("font_color", _GOLD) btn.add_theme_color_override("font_hover_color", _CREAM) btn.add_theme_font_size_override("font_size", 18) return btn