class_name OnlineLobby extends CanvasLayer ## OnlineLobby - Matchmaking UI for ranked queue and private rooms signal game_starting(game_data: Dictionary) signal back_pressed signal profile_requested signal leaderboard_requested # Window dimensions const WINDOW_SIZE := Vector2i(600, 700) # UI Components var main_container: VBoxContainer var back_button: Button var header_container: HBoxContainer var username_label: Label var elo_label: Label var deck_section: VBoxContainer var deck_dropdown: OptionButton var ranked_section: PanelContainer var ranked_content: VBoxContainer var find_match_button: Button var cancel_search_button: Button var queue_status_label: Label var private_section: PanelContainer var private_content: VBoxContainer var create_room_button: Button var join_container: HBoxContainer var room_code_input: LineEdit var join_room_button: Button var room_section: PanelContainer var room_content: VBoxContainer var room_code_label: Label var copy_code_button: Button var host_label: Label var guest_label: Label var ready_button: Button var leave_room_button: Button var error_label: Label var nav_buttons_container: HBoxContainer var profile_button: Button var leaderboard_button: Button # State var is_in_queue: bool = false var is_in_room: bool = false var is_ready: bool = false var queue_start_time: float = 0.0 var current_room_code: String = "" var selected_deck_id: String = "" # Styling var custom_font: Font = preload("res://JimNightshade-Regular.ttf") const BG_COLOR := Color(0.12, 0.11, 0.15, 1.0) const PANEL_COLOR := Color(0.18, 0.16, 0.22, 1.0) const ACCENT_COLOR := Color(0.4, 0.35, 0.55, 1.0) const TEXT_COLOR := Color(0.9, 0.88, 0.82, 1.0) const MUTED_COLOR := Color(0.6, 0.58, 0.52, 1.0) func _ready() -> void: _create_ui() _connect_network_signals() _update_user_info() _fetch_decks() func _process(_delta: float) -> void: # Update queue timer if searching if is_in_queue: var elapsed = Time.get_ticks_msec() / 1000.0 - queue_start_time var minutes = int(elapsed) / 60 var seconds = int(elapsed) % 60 queue_status_label.text = "Searching... %d:%02d" % [minutes, seconds] func _create_ui() -> void: # Background var bg = ColorRect.new() bg.color = BG_COLOR bg.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(bg) # Main container main_container = VBoxContainer.new() main_container.set_anchors_preset(Control.PRESET_FULL_RECT) main_container.add_theme_constant_override("separation", 16) add_child(main_container) var margin = MarginContainer.new() margin.add_theme_constant_override("margin_left", 24) margin.add_theme_constant_override("margin_right", 24) margin.add_theme_constant_override("margin_top", 16) margin.add_theme_constant_override("margin_bottom", 16) margin.set_anchors_preset(Control.PRESET_FULL_RECT) main_container.add_child(margin) var content = VBoxContainer.new() content.add_theme_constant_override("separation", 16) margin.add_child(content) # Back button back_button = _create_button("< Back", false) back_button.custom_minimum_size = Vector2(80, 32) back_button.pressed.connect(_on_back_pressed) content.add_child(back_button) # Header with username and ELO header_container = HBoxContainer.new() header_container.add_theme_constant_override("separation", 16) content.add_child(header_container) username_label = Label.new() username_label.add_theme_font_override("font", custom_font) username_label.add_theme_font_size_override("font_size", 24) username_label.add_theme_color_override("font_color", TEXT_COLOR) username_label.text = "Welcome!" username_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL header_container.add_child(username_label) elo_label = Label.new() elo_label.add_theme_font_override("font", custom_font) elo_label.add_theme_font_size_override("font_size", 20) elo_label.add_theme_color_override("font_color", ACCENT_COLOR) elo_label.text = "ELO: 1000" header_container.add_child(elo_label) # Deck selection section deck_section = VBoxContainer.new() deck_section.add_theme_constant_override("separation", 8) content.add_child(deck_section) var deck_label = Label.new() deck_label.add_theme_font_override("font", custom_font) deck_label.add_theme_font_size_override("font_size", 16) deck_label.add_theme_color_override("font_color", MUTED_COLOR) deck_label.text = "SELECT DECK" deck_section.add_child(deck_label) deck_dropdown = OptionButton.new() deck_dropdown.add_theme_font_override("font", custom_font) deck_dropdown.add_theme_font_size_override("font_size", 16) deck_dropdown.custom_minimum_size = Vector2(0, 40) deck_dropdown.item_selected.connect(_on_deck_selected) deck_section.add_child(deck_dropdown) # Ranked match section ranked_section = _create_panel_section("RANKED MATCH") content.add_child(ranked_section) ranked_content = ranked_section.get_child(0) as VBoxContainer var ranked_desc = Label.new() ranked_desc.add_theme_font_override("font", custom_font) ranked_desc.add_theme_font_size_override("font_size", 14) ranked_desc.add_theme_color_override("font_color", MUTED_COLOR) ranked_desc.text = "Find opponents near your skill level" ranked_content.add_child(ranked_desc) find_match_button = _create_button("Find Match", true) find_match_button.pressed.connect(_on_find_match_pressed) ranked_content.add_child(find_match_button) cancel_search_button = _create_button("Cancel Search", false) cancel_search_button.pressed.connect(_on_cancel_search_pressed) cancel_search_button.visible = false ranked_content.add_child(cancel_search_button) queue_status_label = Label.new() queue_status_label.add_theme_font_override("font", custom_font) queue_status_label.add_theme_font_size_override("font_size", 14) queue_status_label.add_theme_color_override("font_color", ACCENT_COLOR) queue_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER queue_status_label.visible = false ranked_content.add_child(queue_status_label) # Private match section private_section = _create_panel_section("PRIVATE MATCH") content.add_child(private_section) private_content = private_section.get_child(0) as VBoxContainer create_room_button = _create_button("Create Room", true) create_room_button.pressed.connect(_on_create_room_pressed) private_content.add_child(create_room_button) var separator_label = Label.new() separator_label.add_theme_font_override("font", custom_font) separator_label.add_theme_font_size_override("font_size", 12) separator_label.add_theme_color_override("font_color", MUTED_COLOR) separator_label.text = "─────────── OR ───────────" separator_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER private_content.add_child(separator_label) join_container = HBoxContainer.new() join_container.add_theme_constant_override("separation", 8) private_content.add_child(join_container) var code_label = Label.new() code_label.add_theme_font_override("font", custom_font) code_label.add_theme_font_size_override("font_size", 14) code_label.add_theme_color_override("font_color", TEXT_COLOR) code_label.text = "Code:" join_container.add_child(code_label) room_code_input = LineEdit.new() room_code_input.add_theme_font_override("font", custom_font) room_code_input.add_theme_font_size_override("font_size", 16) room_code_input.placeholder_text = "ABC123" room_code_input.max_length = 6 room_code_input.custom_minimum_size = Vector2(100, 36) room_code_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL room_code_input.text_changed.connect(_on_room_code_changed) join_container.add_child(room_code_input) join_room_button = _create_button("Join", false) join_room_button.custom_minimum_size = Vector2(80, 36) join_room_button.pressed.connect(_on_join_room_pressed) join_container.add_child(join_room_button) # Room section (shown when in a room) room_section = _create_panel_section("ROOM") room_section.visible = false content.add_child(room_section) room_content = room_section.get_child(0) as VBoxContainer var room_header = HBoxContainer.new() room_header.add_theme_constant_override("separation", 8) room_content.add_child(room_header) room_code_label = Label.new() room_code_label.add_theme_font_override("font", custom_font) room_code_label.add_theme_font_size_override("font_size", 20) room_code_label.add_theme_color_override("font_color", ACCENT_COLOR) room_code_label.text = "Room: ------" room_code_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL room_header.add_child(room_code_label) copy_code_button = _create_button("Copy", false) copy_code_button.custom_minimum_size = Vector2(60, 28) copy_code_button.pressed.connect(_on_copy_code_pressed) room_header.add_child(copy_code_button) var players_container = VBoxContainer.new() players_container.add_theme_constant_override("separation", 4) room_content.add_child(players_container) host_label = Label.new() host_label.add_theme_font_override("font", custom_font) host_label.add_theme_font_size_override("font_size", 16) host_label.add_theme_color_override("font_color", TEXT_COLOR) host_label.text = "Host: ---" players_container.add_child(host_label) guest_label = Label.new() guest_label.add_theme_font_override("font", custom_font) guest_label.add_theme_font_size_override("font_size", 16) guest_label.add_theme_color_override("font_color", TEXT_COLOR) guest_label.text = "Guest: Waiting..." players_container.add_child(guest_label) var room_buttons = HBoxContainer.new() room_buttons.add_theme_constant_override("separation", 8) room_content.add_child(room_buttons) ready_button = _create_button("Ready", true) ready_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL ready_button.pressed.connect(_on_ready_pressed) room_buttons.add_child(ready_button) leave_room_button = _create_button("Leave Room", false) leave_room_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL leave_room_button.pressed.connect(_on_leave_room_pressed) room_buttons.add_child(leave_room_button) # Error label error_label = Label.new() error_label.add_theme_font_override("font", custom_font) error_label.add_theme_font_size_override("font_size", 14) error_label.add_theme_color_override("font_color", Color(0.9, 0.3, 0.3)) error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER error_label.autowrap_mode = TextServer.AUTOWRAP_WORD error_label.visible = false content.add_child(error_label) # Navigation buttons (Profile and Leaderboard) nav_buttons_container = HBoxContainer.new() nav_buttons_container.add_theme_constant_override("separation", 16) nav_buttons_container.alignment = BoxContainer.ALIGNMENT_CENTER content.add_child(nav_buttons_container) profile_button = _create_button("Profile", false) profile_button.custom_minimum_size = Vector2(120, 36) profile_button.pressed.connect(_on_profile_pressed) nav_buttons_container.add_child(profile_button) leaderboard_button = _create_button("Leaderboard", false) leaderboard_button.custom_minimum_size = Vector2(120, 36) leaderboard_button.pressed.connect(_on_leaderboard_pressed) nav_buttons_container.add_child(leaderboard_button) func _create_panel_section(title: String) -> PanelContainer: var panel = PanelContainer.new() var style = StyleBoxFlat.new() style.bg_color = PANEL_COLOR style.set_corner_radius_all(8) style.set_content_margin_all(16) panel.add_theme_stylebox_override("panel", style) var vbox = VBoxContainer.new() vbox.add_theme_constant_override("separation", 12) panel.add_child(vbox) var title_label = Label.new() title_label.add_theme_font_override("font", custom_font) title_label.add_theme_font_size_override("font_size", 18) title_label.add_theme_color_override("font_color", TEXT_COLOR) title_label.text = title title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vbox.add_child(title_label) return panel func _create_button(text: String, primary: bool) -> Button: var button = Button.new() button.text = text button.add_theme_font_override("font", custom_font) button.add_theme_font_size_override("font_size", 16) button.custom_minimum_size = Vector2(0, 40) var normal = StyleBoxFlat.new() var hover = StyleBoxFlat.new() var pressed = StyleBoxFlat.new() var disabled = StyleBoxFlat.new() if primary: normal.bg_color = ACCENT_COLOR hover.bg_color = ACCENT_COLOR.lightened(0.15) pressed.bg_color = ACCENT_COLOR.darkened(0.15) button.add_theme_color_override("font_color", Color.WHITE) button.add_theme_color_override("font_hover_color", Color.WHITE) else: normal.bg_color = Color(0.25, 0.23, 0.3) hover.bg_color = Color(0.3, 0.28, 0.35) pressed.bg_color = Color(0.2, 0.18, 0.25) button.add_theme_color_override("font_color", TEXT_COLOR) button.add_theme_color_override("font_hover_color", TEXT_COLOR) disabled.bg_color = Color(0.2, 0.18, 0.22) button.add_theme_color_override("font_disabled_color", MUTED_COLOR) for style in [normal, hover, pressed, disabled]: style.set_corner_radius_all(6) style.set_content_margin_all(8) button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", hover) button.add_theme_stylebox_override("pressed", pressed) button.add_theme_stylebox_override("disabled", disabled) return button func _connect_network_signals() -> void: NetworkManager.queue_joined.connect(_on_queue_joined) NetworkManager.queue_left.connect(_on_queue_left) NetworkManager.room_created.connect(_on_room_created) NetworkManager.room_joined.connect(_on_room_joined) NetworkManager.room_updated.connect(_on_room_updated) NetworkManager.matchmaking_update.connect(_on_matchmaking_update) NetworkManager.match_found.connect(_on_match_found) NetworkManager.network_error.connect(_on_network_error) func _disconnect_network_signals() -> void: if NetworkManager.queue_joined.is_connected(_on_queue_joined): NetworkManager.queue_joined.disconnect(_on_queue_joined) if NetworkManager.queue_left.is_connected(_on_queue_left): NetworkManager.queue_left.disconnect(_on_queue_left) if NetworkManager.room_created.is_connected(_on_room_created): NetworkManager.room_created.disconnect(_on_room_created) if NetworkManager.room_joined.is_connected(_on_room_joined): NetworkManager.room_joined.disconnect(_on_room_joined) if NetworkManager.room_updated.is_connected(_on_room_updated): NetworkManager.room_updated.disconnect(_on_room_updated) if NetworkManager.matchmaking_update.is_connected(_on_matchmaking_update): NetworkManager.matchmaking_update.disconnect(_on_matchmaking_update) if NetworkManager.match_found.is_connected(_on_match_found): NetworkManager.match_found.disconnect(_on_match_found) if NetworkManager.network_error.is_connected(_on_network_error): NetworkManager.network_error.disconnect(_on_network_error) func _update_user_info() -> void: var user = NetworkManager.current_user if user.has("username"): username_label.text = "Welcome, %s!" % user.username if user.has("stats") and user.stats.has("elo_rating"): elo_label.text = "ELO: %d" % user.stats.elo_rating else: elo_label.text = "ELO: 1000" func _fetch_decks() -> void: deck_dropdown.clear() deck_dropdown.add_item("-- Select a Deck --", 0) # Add decks from user profile var user = NetworkManager.current_user if user.has("decks") and user.decks is Array: for i in range(user.decks.size()): var deck = user.decks[i] deck_dropdown.add_item(deck.name, i + 1) deck_dropdown.set_item_metadata(i + 1, deck.id) # Also add local starter decks as fallback if deck_dropdown.item_count <= 1: var starter_decks = _get_local_decks() for i in range(starter_decks.size()): deck_dropdown.add_item(starter_decks[i].name, i + 1) deck_dropdown.set_item_metadata(i + 1, "local_%d" % i) func _get_local_decks() -> Array: # Load starter decks from local file var decks = [] var file_path = "res://data/starter_decks.json" if FileAccess.file_exists(file_path): var file = FileAccess.open(file_path, FileAccess.READ) if file: var json = JSON.new() var result = json.parse(file.get_as_text()) if result == OK and json.data is Dictionary: if json.data.has("decks"): decks = json.data.decks file.close() return decks func _show_error(message: String) -> void: error_label.text = message error_label.visible = true # Auto-hide after 5 seconds await get_tree().create_timer(5.0).timeout if is_instance_valid(error_label): error_label.visible = false func _update_ui_state() -> void: # Update button states based on current state var has_deck = selected_deck_id != "" # Queue UI find_match_button.visible = not is_in_queue and not is_in_room find_match_button.disabled = not has_deck cancel_search_button.visible = is_in_queue queue_status_label.visible = is_in_queue # Private match UI private_section.visible = not is_in_queue and not is_in_room create_room_button.disabled = not has_deck join_room_button.disabled = not has_deck or room_code_input.text.length() != 6 # Room UI room_section.visible = is_in_room # Disable ranked section when in room ranked_section.visible = not is_in_room # ========== BUTTON HANDLERS ========== func _on_profile_pressed() -> void: profile_requested.emit() func _on_leaderboard_pressed() -> void: leaderboard_requested.emit() func _on_back_pressed() -> void: # Leave queue or room before going back if is_in_queue: NetworkManager.leave_queue() if is_in_room: NetworkManager.leave_room() _disconnect_network_signals() back_pressed.emit() func _on_deck_selected(index: int) -> void: if index == 0: selected_deck_id = "" else: selected_deck_id = str(deck_dropdown.get_item_metadata(index)) _update_ui_state() func _on_room_code_changed(_new_text: String) -> void: room_code_input.text = room_code_input.text.to_upper() _update_ui_state() func _on_find_match_pressed() -> void: if selected_deck_id == "": _show_error("Please select a deck first") return # Connect to WebSocket if not connected if NetworkManager.connection_state < NetworkManager.ConnectionState.CONNECTED: NetworkManager.connect_websocket() await NetworkManager.connection_state_changed # Wait a bit for auth await get_tree().create_timer(0.5).timeout NetworkManager.join_queue(selected_deck_id) func _on_cancel_search_pressed() -> void: NetworkManager.leave_queue() func _on_create_room_pressed() -> void: if selected_deck_id == "": _show_error("Please select a deck first") return # Connect to WebSocket if not connected if NetworkManager.connection_state < NetworkManager.ConnectionState.CONNECTED: NetworkManager.connect_websocket() await NetworkManager.connection_state_changed await get_tree().create_timer(0.5).timeout NetworkManager.create_room(selected_deck_id) func _on_join_room_pressed() -> void: var code = room_code_input.text.strip_edges().to_upper() if code.length() != 6: _show_error("Room code must be 6 characters") return if selected_deck_id == "": _show_error("Please select a deck first") return # Connect to WebSocket if not connected if NetworkManager.connection_state < NetworkManager.ConnectionState.CONNECTED: NetworkManager.connect_websocket() await NetworkManager.connection_state_changed await get_tree().create_timer(0.5).timeout NetworkManager.join_room(code, selected_deck_id) func _on_copy_code_pressed() -> void: DisplayServer.clipboard_set(current_room_code) copy_code_button.text = "Copied!" await get_tree().create_timer(1.5).timeout if is_instance_valid(copy_code_button): copy_code_button.text = "Copy" func _on_ready_pressed() -> void: is_ready = not is_ready ready_button.text = "Not Ready" if is_ready else "Ready" NetworkManager.set_room_ready(is_ready) func _on_leave_room_pressed() -> void: NetworkManager.leave_room() # ========== NETWORK SIGNAL HANDLERS ========== func _on_queue_joined() -> void: is_in_queue = true queue_start_time = Time.get_ticks_msec() / 1000.0 queue_status_label.text = "Searching... 0:00" _update_ui_state() func _on_queue_left() -> void: is_in_queue = false _update_ui_state() func _on_room_created(room_data: Dictionary) -> void: is_in_room = true current_room_code = room_data.get("code", "") _update_room_display(room_data) _update_ui_state() func _on_room_joined(room_data: Dictionary) -> void: is_in_room = true current_room_code = room_data.get("code", "") _update_room_display(room_data) _update_ui_state() func _on_room_updated(room_data: Dictionary) -> void: _update_room_display(room_data) func _update_room_display(room_data: Dictionary) -> void: room_code_label.text = "Room: %s" % room_data.get("code", "------") var host = room_data.get("host", {}) var host_ready = " ✓" if host.get("ready", false) else "" host_label.text = "Host: %s%s" % [host.get("username", "---"), host_ready] var guest = room_data.get("guest", null) if guest: var guest_ready = " ✓" if guest.get("ready", false) else "" guest_label.text = "Guest: %s%s" % [guest.get("username", "---"), guest_ready] ready_button.disabled = false else: guest_label.text = "Guest: Waiting for opponent..." ready_button.disabled = true func _on_matchmaking_update(data: Dictionary) -> void: var update_type = data.get("type", "") match update_type: "queue_left": is_in_queue = false _update_ui_state() "room_left": is_in_room = false is_ready = false ready_button.text = "Ready" current_room_code = "" room_code_input.text = "" _update_ui_state() var reason = data.get("reason", "") if reason != "": _show_error(reason) func _on_match_found(game_data: Dictionary) -> void: print("Match found! Game ID: ", game_data.get("game_id", "")) is_in_queue = false is_in_room = false _disconnect_network_signals() game_starting.emit(game_data) func _on_network_error(error: String) -> void: _show_error(error) # ========== CLEANUP ========== func _exit_tree() -> void: _disconnect_network_signals()