commit 7ca11c6534122abd45ff0505d057fd1f23b18645 Author: adtpdn Date: Tue May 12 17:55:53 2026 +0800 feat: 2.3.1 diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 18e9b85..5d34bc0 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -30,6 +30,7 @@ extends Control @onready var shop_btn = %CartBtn @onready var top_right_profile_btn = %ProfileBtn @onready var mailbox_btn = get_node_or_null("%MailboxBtn") +@onready var mail_badge = get_node_or_null("%MailBadge") @onready var banner1_btn = get_node_or_null("%Banner1") @onready var ticket_btn = get_node_or_null("%TicketBtn") @onready var mailbox_panel = get_node_or_null("MailboxPanel") @@ -869,28 +870,37 @@ func _on_profile_btn_pressed() -> void: main_menu_panel.hide() profile_panel_instance.show_panel() -func _on_mailbox_pressed() -> void: - if mailbox_panel: - mailbox_panel.show_panel() - if main_menu_panel: - main_menu_panel.hide() - - # Connect the closed signal to reshow main menu if not connected - if not mailbox_panel.closed.is_connected(_on_mailbox_closed): - mailbox_panel.closed.connect(_on_mailbox_closed) +var _mailbox_panel_instance: Control -func _on_mailbox_closed() -> void: - if main_menu_panel: - main_menu_panel.show() +func _on_mailbox_pressed() -> void: + if not _mailbox_panel_instance: + var scene = load("res://scenes/ui/mailbox_panel.tscn") + if scene: + _mailbox_panel_instance = scene.instantiate() + _mailbox_panel_instance.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + + var cl := CanvasLayer.new() + cl.layer = 100 + cl.name = "MailboxCanvasLayer" + add_child(cl) + cl.add_child(_mailbox_panel_instance) + + if _mailbox_panel_instance.has_signal("closed"): + _mailbox_panel_instance.closed.connect(func(): + _mailbox_panel_instance.get_parent().queue_free() + _mailbox_panel_instance = null + ) + + if _mailbox_panel_instance: + _mailbox_panel_instance.show_panel() func _on_mail_unread_count_changed(count: int) -> void: - if mailbox_btn: + if mail_badge: if count > 0: - mailbox_btn.text = "MAIL (%d)" % count - mailbox_btn.add_theme_color_override("font_color", Color.YELLOW) + mail_badge.text = str(count) if count < 100 else "99+" + mail_badge.visible = true else: - mailbox_btn.text = "MAIL" - mailbox_btn.remove_theme_color_override("font_color") + mail_badge.visible = false func _on_logout_pressed() -> void: AuthManager.logout() commit 13f3c3d591ceecf3ce8248b6ba3bc2a9447e3833 Author: adtpdn Date: Mon May 11 17:24:47 2026 +0800 feat: 2.3.1 diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 4026195..18e9b85 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -29,17 +29,28 @@ extends Control @onready var leaderboard_btn = %LeaderboardBtn @onready var shop_btn = %CartBtn @onready var top_right_profile_btn = %ProfileBtn -@onready var banner1_btn = %Banner1 -@onready var ticket_btn = $MainMenuPanel/MainMargin/MainHBox/RightCol/TopRightPanel/TicketBtn +@onready var mailbox_btn = get_node_or_null("%MailboxBtn") +@onready var banner1_btn = get_node_or_null("%Banner1") +@onready var ticket_btn = get_node_or_null("%TicketBtn") +@onready var mailbox_panel = get_node_or_null("MailboxPanel") # UI References - Room List -@onready var room_list_panel = $RoomListPanel -@onready var room_list = $RoomListPanel/VBoxContainer/RoomList -@onready var match_id_input = $RoomListPanel/VBoxContainer/MatchIdInput -@onready var refresh_btn = $RoomListPanel/VBoxContainer/ButtonContainer/RefreshBtn -@onready var join_btn = $RoomListPanel/VBoxContainer/ButtonContainer/JoinBtn -@onready var back_btn = $RoomListPanel/VBoxContainer/ButtonContainer/BackBtn -@onready var room_list_profile_btn = $RoomListPanel/VBoxContainer/ButtonContainer/ProfileBtn +@onready var room_list_panel = %RoomListPanel +@onready var room_list = get_node_or_null("%RoomList") +@onready var match_id_input = get_node_or_null("%MatchIdInput") +@onready var refresh_btn = get_node_or_null("%RefreshBtn") +@onready var join_btn = get_node_or_null("%JoinBtn") +@onready var back_btn = get_node_or_null("%RoomListCloseBtn") +@onready var room_list_profile_btn = get_node_or_null("%RoomListProfileBtn") +@onready var item_template = get_node_or_null("%ItemTemplate") + +@onready var room_player_username = get_node_or_null("%PlayerUsername") +@onready var room_player_score = get_node_or_null("%PlayerScore") +@onready var room_player_rank = get_node_or_null("%Rank") +@onready var room_avatar = get_node_or_null("%Avatar") + +# Cached leaderboard rank for local player +var _local_player_rank: int = 0 # UI References - Lobby Panel @onready var lobby_panel = $LobbyPanel @@ -124,6 +135,9 @@ var daily_reward_panel_instance: Control # Bot name tracking keyed by slot index to avoid re-generating on each update var _bot_names: Dictionary = {} +# Room list filter ("" = all, "Freemode", "Stop n Go", etc.) +var _room_mode_filter: String = "" + # ============================================================================= # Chat System # ============================================================================= @@ -135,6 +149,9 @@ var _chat_messages: Array = [] @onready var chat_input: LineEdit = %ChatInput @onready var chat_send_btn: Button = %SendBtn +var _friend_suggest_panel: PanelContainer +var _friend_suggest_list: ItemList + # Server Selection Controls (Now in tscn) # var server_option: OptionButton # var server_ip_input: LineEdit @@ -167,6 +184,8 @@ func _ready(): main_menu_profile_btn.pressed.connect(_on_profile_btn_pressed) if top_right_profile_btn: top_right_profile_btn.pressed.connect(_on_profile_btn_pressed) + if mailbox_btn: + mailbox_btn.pressed.connect(_on_mailbox_pressed) if lobby_settings_btn: lobby_settings_btn.pressed.connect(_on_settings_pressed) @@ -191,6 +210,16 @@ func _ready(): var social_btn = get_node_or_null("%SocialBtn") if social_btn: social_btn.pressed.connect(_on_social_pressed) + + # Connect Social / Friend UI + var global_chat_tab_btn = get_node_or_null("%GlobalChatTabBtn") + if global_chat_tab_btn: + global_chat_tab_btn.pressed.connect(func(): _switch_chat_tab("global")) + + FriendManager.dm_message_received.connect(_on_lobby_dm_received) + + + # Connect Server Selection signals if server_option: @@ -202,15 +231,53 @@ func _ready(): server_ip_input.focus_exited.connect(func(): _on_server_ip_submitted(server_ip_input.text)) # Connect button signals - Room List - refresh_btn.pressed.connect(_on_refresh_pressed) - join_btn.pressed.connect(_on_join_pressed) - back_btn.pressed.connect(_on_back_pressed) + if refresh_btn: refresh_btn.pressed.connect(_on_refresh_pressed) + if join_btn: join_btn.pressed.connect(_on_join_pressed) + if back_btn: back_btn.pressed.connect(_on_back_pressed) + if match_id_input: + match_id_input.text_submitted.connect(func(_text): _on_join_pressed()) if room_list: room_list.item_selected.connect(_on_room_selected) room_list.item_activated.connect(_on_room_activated) if room_list_profile_btn: room_list_profile_btn.pressed.connect(_on_profile_btn_pressed) + + # Connect Side Tab switching + var play_side_btn = get_node_or_null("%PlayTabSideBtn") + var room_side_btn = get_node_or_null("%RoomTabSideBtn") + var room_tabs = get_node_or_null("%RoomListTabs") + if play_side_btn and room_side_btn and room_tabs: + play_side_btn.pressed.connect(func(): + room_tabs.current_tab = 0 + play_side_btn.button_pressed = true + room_side_btn.button_pressed = false + ) + room_side_btn.pressed.connect(func(): + room_tabs.current_tab = 1 + play_side_btn.button_pressed = false + room_side_btn.button_pressed = true + ) + + # Connect Play Tab mode buttons + var free_mode_btn = get_node_or_null("%FreeModeBtn") + var stop_n_go_btn = get_node_or_null("%StopNGoBtn") + var room_free_mode_btn = get_node_or_null("%RoomFreeModeBtn") + var room_stop_n_go_btn = get_node_or_null("%RoomStopNGoBtn") + if free_mode_btn: + free_mode_btn.pressed.connect(func(): _host_room("Freemode")) + if stop_n_go_btn: + stop_n_go_btn.pressed.connect(func(): _host_room("Stop n Go")) + if room_free_mode_btn: + room_free_mode_btn.pressed.connect(func(): + _room_mode_filter = "Freemode" if _room_mode_filter != "Freemode" else "" + LobbyManager.refresh_room_list() + ) + if room_stop_n_go_btn: + room_stop_n_go_btn.pressed.connect(func(): + _room_mode_filter = "Stop n Go" if _room_mode_filter != "Stop n Go" else "" + LobbyManager.refresh_room_list() + ) # Connect button signals - Lobby profile_btn.pressed.connect(_on_profile_btn_pressed) @@ -274,6 +341,13 @@ func _ready(): chat_send_btn.pressed.connect(_on_chat_send_pressed) if chat_input: chat_input.text_submitted.connect(func(_t): _on_chat_send_pressed()) + chat_input.text_changed.connect(_on_chat_input_changed) + + _setup_friend_suggest_ui() + + # Connect Mailbox UI + if MailManager: + MailManager.unread_count_changed.connect(_on_mail_unread_count_changed) # Connect Social / Friend UI invite_btn = get_node_or_null("LobbyPanel/BottomBar/InviteBtn") @@ -455,38 +529,68 @@ func _on_tutorial_pressed() -> void: LobbyManager.start_game(true) func _on_create_room_pressed() -> void: - # Use profile name for logged-in users, or guest for others + _show_panel("room_list") + var tabs = get_node_or_null("%RoomListTabs") + if tabs: + tabs.current_tab = 0 + if get_node_or_null("%PlayTabSideBtn"): get_node("%PlayTabSideBtn").button_pressed = true + if get_node_or_null("%RoomTabSideBtn"): get_node("%RoomTabSideBtn").button_pressed = false + _sync_room_profile_card() + + # --- PRESERVED HOSTING LOGIC --- + # Moved to _host_room() — called by FreeModeBtn / StopNGoBtn + # ------------------------------- + +func _host_room(game_mode: String) -> void: + """Restore preserved hosting logic. Called by mode buttons. + game_mode: set LobbyManager.game_mode before hosting (e.g. 'Stop n Go'), or 'Freemode' to keep default.""" + # Set player name if AuthManager.is_guest: if LobbyManager.local_player_name.is_empty() or LobbyManager.local_player_name == "Player": LobbyManager.local_player_name = NameGenerator.generate_guest_name() else: LobbyManager.local_player_name = UserProfileManager.get_display_name() + # Set game mode BEFORE is_host flag (set_game_mode guards on is_host) + if not game_mode.is_empty(): + LobbyManager.game_mode = game_mode + LobbyManager._update_available_areas(game_mode) + + _apply_loadout_character() + + var mode_prefix := "[%s] " % game_mode if not game_mode.is_empty() else "" + if LobbyManager.is_lan_mode: connection_status.text = "Starting LAN room..." - # Apply loadout character before creating room - _apply_loadout_character() - var ok = await LobbyManager.create_room_lan("LAN Room " + str(randi_range(100, 999))) + var room_label := "%sLAN Room %d" % [mode_prefix, randi_range(100, 999)] + var ok = await LobbyManager.create_room_lan(room_label) if not ok: connection_status.text = "Failed to start LAN room. Check port 7777." else: connection_status.text = "Creating Nakama room..." - # Apply loadout character before creating room - _apply_loadout_character() - LobbyManager.create_room("Room %d" % randi_range(1000, 9999)) + var room_label := "%sRoom %d" % [mode_prefix, randi_range(1000, 9999)] + LobbyManager.create_room(room_label) func _on_browse_rooms_pressed() -> void: _show_panel("room_list") + var tabs = get_node_or_null("%RoomListTabs") + if tabs: + tabs.current_tab = 1 + if get_node_or_null("%PlayTabSideBtn"): get_node("%PlayTabSideBtn").button_pressed = false + if get_node_or_null("%RoomTabSideBtn"): get_node("%RoomTabSideBtn").button_pressed = true + _sync_room_profile_card() + + var match_id_label = get_node_or_null("%RoomListPanel/RoomListTabs/RoomTab/HBoxContainer/RightCol/MatchIdLabel") if LobbyManager.is_lan_mode: connection_status.text = "LAN Mode - Enter Host IP to join" match_id_input.placeholder_text = "Enter Host IP (e.g. 192.168.1.10)..." - $RoomListPanel/VBoxContainer/MatchIdLabel.text = "DIRECT CONNECT (HOST IP)" + if match_id_label: match_id_label.text = "DIRECT CONNECT (HOST IP)" _on_refresh_pressed() # Try to discover rooms if implemented else: connection_status.text = "Loading Nakama rooms..." match_id_input.placeholder_text = "Paste match ID here..." - $RoomListPanel/VBoxContainer/MatchIdLabel.text = "DIRECT CONNECT (MATCH ID)" + if match_id_label: match_id_label.text = "DIRECT CONNECT (MATCH ID)" LobbyManager.refresh_room_list() # ============================================================================= @@ -765,6 +869,29 @@ func _on_profile_btn_pressed() -> void: main_menu_panel.hide() profile_panel_instance.show_panel() +func _on_mailbox_pressed() -> void: + if mailbox_panel: + mailbox_panel.show_panel() + if main_menu_panel: + main_menu_panel.hide() + + # Connect the closed signal to reshow main menu if not connected + if not mailbox_panel.closed.is_connected(_on_mailbox_closed): + mailbox_panel.closed.connect(_on_mailbox_closed) + +func _on_mailbox_closed() -> void: + if main_menu_panel: + main_menu_panel.show() + +func _on_mail_unread_count_changed(count: int) -> void: + if mailbox_btn: + if count > 0: + mailbox_btn.text = "MAIL (%d)" % count + mailbox_btn.add_theme_color_override("font_color", Color.YELLOW) + else: + mailbox_btn.text = "MAIL" + mailbox_btn.remove_theme_color_override("font_color") + func _on_logout_pressed() -> void: AuthManager.logout() _go_to_login() @@ -895,17 +1022,75 @@ func _go_to_login() -> void: func _on_room_list_updated(rooms: Array) -> void: room_list.clear() - for room in rooms: - var room_name = room.get("room_name", "Unknown") - var host_name = room.get("host_name", "Unknown") - var player_count = room.get("player_count", 1) - var max_players = room.get("max_players", 4) - room_list.add_item("%s - %s (%d/%d)" % [room_name, host_name, player_count, max_players]) - - if rooms.size() == 0: - connection_status.text = "No rooms available" + # Remove previously cloned row nodes (children after Head and ItemTemplate) + if item_template: + var parent = item_template.get_parent() + for child in parent.get_children(): + if child.has_meta("room_row"): + child.queue_free() + + var filtered := rooms.filter(func(r): + return _room_mode_filter.is_empty() or r.get("game_mode", "") == _room_mode_filter + ) + + if item_template: + # Use visual row template + var parent = item_template.get_parent() + for i in range(filtered.size()): + var room = filtered[i] + var row = item_template.duplicate() + row.visible = true + row.set_meta("room_row", true) + row.set_meta("room_index", i) + + var room_id_short = room.get("room_name", "???") + var host_name = room.get("host_name", "Unknown") + var player_count = room.get("player_count", 1) + var max_players = room.get("max_players", 8) + var mode = room.get("game_mode", "") + var mode_short = "FMD" if mode == "Freemode" else ("SNG" if mode == "Stop n Go" else mode.left(3).to_upper()) + + # Populate labels inside the cloned template + var hbox = row.get_node("PanelContainer/MarginContainer/HBoxContainer") + if hbox: + var lbl_id = hbox.get_child(0) # ListRoomID + var lbl_host = hbox.get_child(1) # ListHost + var lbl_mode = hbox.get_child(2) # ListMode + var lbl_cap = hbox.get_child(3) # ListCap + if lbl_id: lbl_id.text = room_id_short + if lbl_host: lbl_host.text = host_name + if lbl_mode: lbl_mode.text = mode_short + if lbl_cap: lbl_cap.text = "%d/%d" % [player_count, max_players] + + parent.add_child(row) + + # Make row clickable — select in ItemList + fill MatchIdInput + var panel_box = row.get_node_or_null("PanelContainer") + if panel_box: + var idx = i + panel_box.gui_input.connect(func(event: InputEvent): + if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + _on_room_selected(idx) + if event.double_click: + _on_room_activated(idx) + ) + panel_box.mouse_filter = Control.MOUSE_FILTER_STOP else: - connection_status.text = "Found %d room(s)" % rooms.size() + # Fallback: flat strings in ItemList + for room in filtered: + var room_name = room.get("room_name", "Unknown") + var host_name = room.get("host_name", "Unknown") + var player_count = room.get("player_count", 1) + var max_players = room.get("max_players", 4) + var mode = room.get("game_mode", "") + var mode_tag = " [%s]" % mode if not mode.is_empty() else "" + room_list.add_item("%s - %s (%d/%d)%s" % [room_name, host_name, player_count, max_players, mode_tag]) + + var filter_note = " [Filter: %s]" % _room_mode_filter if not _room_mode_filter.is_empty() else "" + if filtered.size() == 0: + connection_status.text = "No rooms available%s" % filter_note + else: + connection_status.text = "Found %d room(s)%s" % [filtered.size(), filter_note] func _on_room_joined(room_data: Dictionary) -> void: _show_panel("lobby") @@ -1051,12 +1236,76 @@ func _on_profile_updated() -> void: gold_label.text = str(UserProfileManager.wallet.get("gold", 0)) if star_label and UserProfileManager.is_profile_loaded: star_label.text = str(UserProfileManager.wallet.get("star", 0)) + + # Update Room tab stats + if room_player_username: + room_player_username.text = display_name + if room_player_score and UserProfileManager.is_profile_loaded: + var total = UserProfileManager.stats.get("total_score", 0) + room_player_score.text = str(total).pad_zeros(6) + if room_player_rank and UserProfileManager.is_profile_loaded: + room_player_rank.text = str(_local_player_rank).pad_zeros(2) + + if room_avatar and avatar_display and avatar_display.texture: + # Copy the style and override it to use the avatar texture + var style = StyleBoxTexture.new() + style.texture = avatar_display.texture + room_avatar.add_theme_stylebox_override("panel", style) # Sync to LobbyManager LobbyManager.set_player_name(display_name) _setup_3d_preview() +# ============================================================================= +# Room Profile Card Sync +# ============================================================================= + +func _sync_room_profile_card() -> void: + """Sync username, total_score, rank, and avatar into the RoomTab profile card.""" + if room_player_username: + room_player_username.text = UserProfileManager.get_display_name() if UserProfileManager.is_profile_loaded else LobbyManager.local_player_name + if room_player_score and UserProfileManager.is_profile_loaded: + var total = UserProfileManager.stats.get("total_score", 0) + room_player_score.text = str(total).pad_zeros(6) + if room_player_rank: + room_player_rank.text = str(_local_player_rank).pad_zeros(2) + if room_avatar and avatar_display and avatar_display.texture: + var style = StyleBoxTexture.new() + style.texture = avatar_display.texture + room_avatar.add_theme_stylebox_override("panel", style) + # Fetch rank from leaderboard in background and update label when done + _fetch_local_player_rank() + +# ============================================================================= +# Leaderboard Rank Fetch +# ============================================================================= + +func _fetch_local_player_rank() -> void: + """Fetch local player's rank from Nakama leaderboard and update the Rank label.""" + if not NakamaManager.session or AuthManager.is_guest: + return + var my_id := NakamaManager.session.user_id + # List around the local player to get their rank + var result = await NakamaManager.client.list_leaderboard_records_around_owner_async( + NakamaManager.session, + "global_high_score", + my_id, + null, + 1 + ) + if result.is_exception(): + return + if result.owner_records and result.owner_records.size() > 0: + var rec = result.owner_records[0] + _local_player_rank = int(rec.rank) + elif result.records and result.records.size() > 0: + _local_player_rank = int(result.records[0].rank) + else: + return + if room_player_rank and room_list_panel.visible: + room_player_rank.text = str(_local_player_rank).pad_zeros(2) + # ============================================================================= # Player Slot Updates # ============================================================================= @@ -1286,26 +1535,75 @@ func _on_invite_accepted() -> void: _pending_invite_match_id = "" func _on_social_pressed() -> void: - """Open social / friend list panel.""" if not social_panel_instance: var scene = load("res://scenes/ui/social_panel.tscn") if scene: social_panel_instance = scene.instantiate() social_panel_instance.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - add_child(social_panel_instance) + + # Wrap in CanvasLayer so it draws above 3D viewport and all lobby UI + var cl := CanvasLayer.new() + cl.layer = 100 + cl.name = "SocialCanvasLayer" + add_child(cl) + cl.add_child(social_panel_instance) + + if social_panel_instance.has_signal("dm_requested"): + social_panel_instance.dm_requested.connect(func(user_id, username): + social_panel_instance.hide() + _open_dm_tab(user_id, username) + # Restore Lobby UI + if main_menu_panel: + var main_hbox = main_menu_panel.get_node_or_null("MainMargin/MainHBox") + if main_hbox: + var left_col = main_hbox.get_node_or_null("LeftCol") + if left_col: + for child in left_col.get_children(): + child.show() + var right_col = main_hbox.get_node_or_null("RightCol") + if right_col: right_col.show() + ) + if social_panel_instance.has_signal("closed"): social_panel_instance.closed.connect(func(): social_panel_instance.hide() - if main_menu_panel: main_menu_panel.show() + # Restore Lobby UI + if main_menu_panel: + var main_hbox = main_menu_panel.get_node_or_null("MainMargin/MainHBox") + if main_hbox: + var left_col = main_hbox.get_node_or_null("LeftCol") + if left_col: + for child in left_col.get_children(): + child.show() + var right_col = main_hbox.get_node_or_null("RightCol") + if right_col: right_col.show() ) if social_panel_instance: - if main_menu_panel: main_menu_panel.hide() + # Hide unnecessary UI to focus on Social/Chat + if main_menu_panel: + var main_hbox = main_menu_panel.get_node_or_null("MainMargin/MainHBox") + if main_hbox: + var left_col = main_hbox.get_node_or_null("LeftCol") + if left_col: + for child in left_col.get_children(): + # Keep Chat, Input, and Spacer (to keep chat at bottom) + if child.name not in ["ChatPanel", "HBoxContainer", "SpacerMiddle"]: + child.hide() + var right_col = main_hbox.get_node_or_null("RightCol") + if right_col: right_col.hide() social_panel_instance.show() + + + # ============================================================================= # Global Chat System # ============================================================================= +var _active_chat_context: String = "global" +var _dm_tabs: Dictionary = {} # user_id -> Control +var _dm_messages: Dictionary = {} # user_id -> Array + func _join_global_chat() -> void: """Join the persistent global lobby chat channel via Nakama socket.""" # Already in the channel — don't rejoin @@ -1388,7 +1686,7 @@ func _add_chat_message(message, refresh_display: bool) -> void: _refresh_chat_display() func _on_chat_send_pressed() -> void: - """Send a message to the global chat channel.""" + """Send a message to the global chat channel or DM.""" if chat_input == null or chat_input.text.strip_edges().is_empty(): return @@ -1396,6 +1694,68 @@ func _on_chat_send_pressed() -> void: chat_input.text = "" chat_input.grab_focus() + if _friend_suggest_panel and _friend_suggest_panel.visible: + _friend_suggest_panel.hide() + + # Check if user used @ shortcut + if text.begins_with("@"): + var space_idx = text.find(" ") + var target_username = "" + var dm_message = "" + + if space_idx > 0: + target_username = text.substr(1, space_idx - 1) + dm_message = text.substr(space_idx + 1).strip_edges() + else: + target_username = text.substr(1).strip_edges() + + if not target_username.is_empty(): + var target_friend = null + for f in FriendManager.friends: + if f.get("username", "") == target_username: + target_friend = f + break + + if target_friend: + var user_id = target_friend.get("user_id", "") + var username = target_friend.get("username", "") + + _open_dm_tab(user_id, username) + + if not dm_message.is_empty(): + _send_dm_message(user_id, dm_message) + return + elif text.begins_with("@"): + _inject_local_message("User %s not found in friends." % target_username) + return + + if _active_chat_context != "global": + if text == "/clear": + _dm_messages[_active_chat_context] = [] + _refresh_chat_display() + return + _send_dm_message(_active_chat_context, text) + return + + # Handle /clear command on global tab + if text == "/clear": + var is_admin = await AdminManager._check_admin_status() + if is_admin: + # Clear local buffer + _chat_messages.clear() + _refresh_chat_display() + # Also clear server-side persisted messages so they don't reload + if _chat_channel and NakamaManager.session and NakamaManager.client: + var payload = JSON.stringify({"channel_id": _chat_channel.id}) + var rpc_result = await NakamaManager.client.rpc_async(NakamaManager.session, "admin_clear_global_chat", payload) + if rpc_result.is_exception(): + push_warning("[Chat] admin_clear_global_chat RPC failed: " + rpc_result.get_exception().message) + else: + _inject_local_message("[SYSTEM] : Global chat cleared by admin.") + else: + _inject_local_message("[SYSTEM] : Unknown cmd, /clear only usable on DM between user.") + return + # Instantly show locally for best UX _inject_local_message(text) @@ -1409,10 +1769,130 @@ func _on_chat_send_pressed() -> void: if result.is_exception(): push_warning("[Chat] Failed to send message: " + result.get_exception().message) +func _send_dm_message(user_id: String, text: String) -> void: + var sent = await FriendManager.send_dm(user_id, text) + if sent: + if not _dm_messages.has(user_id): + _dm_messages[user_id] = [] + _dm_messages[user_id].append({"sender": "You", "content": text, "ts": _get_local_time()}) + if _active_chat_context == user_id: + _refresh_chat_display() + +func _on_lobby_dm_received(from_user_id: String, from_name: String, message: String) -> void: + if not _dm_messages.has(from_user_id): + _dm_messages[from_user_id] = [] + + _dm_messages[from_user_id].append({"sender": from_name, "content": message, "ts": _get_local_time()}) + + if not _dm_tabs.has(from_user_id): + _create_dm_tab(from_user_id, from_name) + + if _active_chat_context == from_user_id: + _refresh_chat_display() + +func _open_dm_tab(user_id: String, username: String) -> void: + if not _dm_tabs.has(user_id): + _create_dm_tab(user_id, username) + _dm_messages[user_id] = [] + _switch_chat_tab(user_id) + + var history = await FriendManager.get_dm_history(user_id) + var my_id = NakamaManager.session.user_id if NakamaManager.session else "" + for entry in history: + var is_self = entry.get("from") == my_id + var sender_name = "You" if is_self else username + var ts = _format_nakama_time(entry.get("create_time", "")) + _dm_messages[user_id].append({"sender": sender_name, "content": entry.get("msg", ""), "ts": ts}) + + if _active_chat_context == user_id: + _refresh_chat_display() + else: + _switch_chat_tab(user_id) + +func _create_dm_tab(user_id: String, username: String) -> void: + var tabs_container = get_node_or_null("%ChatTabsContainer") + var template = get_node_or_null("%DMTabTemplate") + if not tabs_container or not template: return + + var hbox = template.duplicate() + hbox.visible = true + + var btn = hbox.get_node("DMTabBtn") + if btn: + btn.text = username + btn.pressed.connect(func(): _switch_chat_tab(user_id)) + + var close_btn = hbox.get_node("DMTabCloseBtn") + if close_btn: + close_btn.pressed.connect(func(): _close_dm_tab(user_id)) + + tabs_container.add_child(hbox) + _dm_tabs[user_id] = hbox + +func _close_dm_tab(user_id: String) -> void: + if _dm_tabs.has(user_id): + var tab = _dm_tabs[user_id] + tab.queue_free() + _dm_tabs.erase(user_id) + + if _active_chat_context == user_id: + _switch_chat_tab("global") + +func _switch_chat_tab(context_id: String) -> void: + _active_chat_context = context_id + _refresh_chat_display() + + var tabs_container = get_node_or_null("%ChatTabsContainer") + if tabs_container: + var global_btn = get_node_or_null("%GlobalChatTabBtn") + if global_btn: + global_btn.modulate = Color(1.0, 1.0, 1.0) if context_id == "global" else Color(0.6, 0.6, 0.6) + + for u_id in _dm_tabs: + var tab_hbox = _dm_tabs[u_id] + tab_hbox.modulate = Color(1.0, 1.0, 1.0) if context_id == u_id else Color(0.6, 0.6, 0.6) + +func _setup_friend_suggest_ui() -> void: + _friend_suggest_panel = get_node_or_null("%FriendSuggestPanel") + _friend_suggest_list = get_node_or_null("%FriendSuggestList") + if not _friend_suggest_panel or not _friend_suggest_list: + push_warning("[Lobby] FriendSuggestPanel or FriendSuggestList not found in scene") + return + _friend_suggest_panel.visible = false + # item_activated fires on double-click / Enter (desktop) + _friend_suggest_list.item_activated.connect(_on_friend_suggest_activated) + # item_selected fires on single tap (touch / mobile) + _friend_suggest_list.item_selected.connect(_on_friend_suggest_activated) + +func _on_chat_input_changed(new_text: String) -> void: + if not _friend_suggest_panel or not _friend_suggest_list: + return + if new_text.begins_with("@") and not " " in new_text: + var search = new_text.substr(1).to_lower() + _friend_suggest_list.clear() + for f in FriendManager.friends: + if f.get("state", -1) == FriendManager.STATE_FRIEND: + var uname = f.get("username", "") + if uname.to_lower().begins_with(search) or search.is_empty(): + _friend_suggest_list.add_item(uname) + + _friend_suggest_panel.visible = _friend_suggest_list.item_count > 0 + else: + _friend_suggest_panel.visible = false + +func _on_friend_suggest_activated(index: int) -> void: + if not _friend_suggest_list: + return + var uname = _friend_suggest_list.get_item_text(index) + chat_input.text = "@%s " % uname + chat_input.caret_column = chat_input.text.length() + chat_input.grab_focus() + _friend_suggest_panel.visible = false + func _inject_local_message(text: String) -> void: """Display a message as the local player when offline/fallback.""" var display_name = UserProfileManager.get_display_name("You") - var ts_str = _get_local_time_hhmm() + var ts_str = _get_local_time() _chat_messages.append({ "sender": display_name, "content": text, @@ -1432,7 +1912,15 @@ func _refresh_chat_display() -> void: return chat_display.clear() - for msg in _chat_messages: + + var messages_to_show = _chat_messages + if _active_chat_context != "global": + if _dm_messages.has(_active_chat_context): + messages_to_show = _dm_messages[_active_chat_context] + else: + messages_to_show = [] + + for msg in messages_to_show: var ts: String = msg.get("ts", "") var sender: String = msg.get("sender", "?") var text: String = msg.get("content", "") @@ -1448,27 +1936,29 @@ func _refresh_chat_display() -> void: chat_display.scroll_to_line(chat_display.get_line_count()) func _format_nakama_time(time_str: String) -> String: - """Convert Nakama time to local 'HH:MM'.""" - # Nakama returns UNIX epoch as string (e.g. "1714418656") or ISO string. + """Convert Nakama time to local 'DD-MM-YY - HH:MM'.""" if time_str.is_valid_int(): var unix_time = time_str.to_int() - var dict = Time.get_time_dict_from_unix_time(unix_time) - return "%02d:%02d" % [dict.hour, dict.minute] + var d = Time.get_date_dict_from_unix_time(unix_time) + var t = Time.get_time_dict_from_unix_time(unix_time) + return "%02d-%02d-%02d - %02d:%02d" % [d.day, d.month, d.year % 100, t.hour, t.minute] # Fallback for ISO strings or empty - if time_str.length() < 19: - return _get_local_time_hhmm() - var t_parts = time_str.split("T") - if t_parts.size() < 2: - return _get_local_time_hhmm() - var time_part = t_parts[1].replace("Z", "").split(":") - if time_part.size() < 2: - return _get_local_time_hhmm() - return "%s:%s" % [time_part[0], time_part[1]] - -func _get_local_time_hhmm() -> String: + if time_str.length() >= 19: + var date_part = time_str.substr(0, 10).split("-") + var time_part = time_str.substr(11, 5) # HH:MM + if date_part.size() == 3: + var year = date_part[0].substr(2, 2) + var month = date_part[1] + var day = date_part[2] + return "%s-%s-%s - %s" % [day, month, year, time_part] + + return _get_local_time() + +func _get_local_time() -> String: + var d = Time.get_date_dict_from_system() var t = Time.get_time_dict_from_system() - return "%02d:%02d" % [t.hour, t.minute] + return "%02d-%02d-%02d - %02d:%02d" % [d.day, d.month, d.year % 100, t.hour, t.minute] func _leave_global_chat() -> void: """Leave the chat channel cleanly.""" commit 21875cdf8af41ecdb477b912301f184dc71661fb Author: adtpdn Date: Fri May 1 05:07:54 2026 +0800 feat: add dailylogin feature diff --git a/scenes/lobby.gd b/scenes/lobby.gd index e8e34d5..4026195 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -30,6 +30,7 @@ extends Control @onready var shop_btn = %CartBtn @onready var top_right_profile_btn = %ProfileBtn @onready var banner1_btn = %Banner1 +@onready var ticket_btn = $MainMenuPanel/MainMargin/MainHBox/RightCol/TopRightPanel/TicketBtn # UI References - Room List @onready var room_list_panel = $RoomListPanel @@ -118,6 +119,7 @@ var current_match_id: String = "" var leaderboard_panel_instance: Control var shop_panel_instance: Control +var daily_reward_panel_instance: Control # Bot name tracking keyed by slot index to avoid re-generating on each update var _bot_names: Dictionary = {} @@ -180,6 +182,8 @@ func _ready(): if leaderboard_btn: leaderboard_btn.pressed.connect(_on_leaderboard_pressed) + if ticket_btn: + ticket_btn.pressed.connect(_on_ticket_pressed) if quit_btn: quit_btn.pressed.connect(_on_quit_pressed) @@ -862,6 +866,25 @@ func _on_leaderboard_pressed() -> void: main_menu_panel.hide() leaderboard_panel_instance.show_panel() +func _on_ticket_pressed() -> void: + if not NakamaManager.session: + connection_status.text = "Must be logged in" + return + + if not daily_reward_panel_instance: + var scene = load("res://scenes/ui/daily_reward_panel.tscn") + if scene: + daily_reward_panel_instance = scene.instantiate() + daily_reward_panel_instance.closed.connect(func(): + if main_menu_panel: main_menu_panel.show() + ) + add_child(daily_reward_panel_instance) + + if daily_reward_panel_instance: + if main_menu_panel: + main_menu_panel.hide() + daily_reward_panel_instance.show_panel() + func _go_to_login() -> void: if get_tree(): get_tree().change_scene_to_file("res://scenes/ui/login_screen.tscn") commit 54be7bbb2545e68016f3fba33b15fec368bd3f35 Author: adtpdn Date: Thu Apr 30 04:18:46 2026 +0800 feat: bug fix social system diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 5af709b..e8e34d5 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -125,7 +125,7 @@ var _bot_names: Dictionary = {} # ============================================================================= # Chat System # ============================================================================= -const GLOBAL_CHAT_ROOM := "global_lobby" +const GLOBAL_CHAT_ROOM := "social_global" var _chat_channel = null var _chat_messages: Array = [] @@ -1311,15 +1311,30 @@ func _join_global_chat() -> void: if not socket.received_channel_message.is_connected(_on_chat_message_received): socket.received_channel_message.connect(_on_chat_message_received) - # Load history and render (Nakama sends recent messages on join via received_channel_message) + # Load history + _chat_messages.clear() + var history_result = await NakamaManager.client.list_channel_messages_async(NakamaManager.session, _chat_channel.id, 50, false) + if not history_result.is_exception() and history_result.messages: + var msgs = history_result.messages.duplicate() + msgs.reverse() # Oldest to newest + for msg in msgs: + _add_chat_message(msg, false) + + _trim_old_messages() _refresh_chat_display() func _on_chat_message_received(message) -> void: """Nakama socket signal: a message arrived on any channel.""" - # message is ApiChannelMessage — use direct property access, NOT .get() if _chat_channel == null or message.channel_id != _chat_channel.id: return + # Ignore messages from ourselves (we inject them locally for instant feedback) + if NakamaManager.session and message.sender_id == NakamaManager.session.user_id: + return + + _add_chat_message(message, true) + +func _add_chat_message(message, refresh_display: bool) -> void: # content is a String (JSON) — parse to extract our "msg" field var text: String = "" var parsed = JSON.parse_string(message.content) @@ -1330,8 +1345,10 @@ func _on_chat_message_received(message) -> void: # Sender: use username property directly (falls back to first 8 chars of sender_id) var sender: String = message.username - if sender.is_empty(): + if sender.is_empty() and message.sender_id: sender = message.sender_id.substr(0, 8) + elif sender.is_empty(): + sender = "Unknown" # Timestamp → HH:MM var ts_str: String = _format_nakama_time(message.create_time) @@ -1340,11 +1357,12 @@ func _on_chat_message_received(message) -> void: "sender": sender, "content": text, "ts": ts_str, - "date": message.create_time.substr(0, 10) + "date": message.create_time.substr(0, 10) if message.create_time else Time.get_date_string_from_system() }) - _trim_old_messages() - _refresh_chat_display() + if refresh_display: + _trim_old_messages() + _refresh_chat_display() func _on_chat_send_pressed() -> void: """Send a message to the global chat channel.""" @@ -1355,10 +1373,11 @@ func _on_chat_send_pressed() -> void: chat_input.text = "" chat_input.grab_focus() + # Instantly show locally for best UX + _inject_local_message(text) + var socket = NakamaManager.socket if not socket or _chat_channel == null: - # Offline fallback: show locally only - _inject_local_message(text) return # Nakama GDScript SDK: write_chat_message_async takes a Dictionary, not a JSON string @@ -1366,8 +1385,6 @@ func _on_chat_send_pressed() -> void: var result = await socket.write_chat_message_async(_chat_channel.id, content) if result.is_exception(): push_warning("[Chat] Failed to send message: " + result.get_exception().message) - # Still show it locally - _inject_local_message(text) func _inject_local_message(text: String) -> void: """Display a message as the local player when offline/fallback.""" @@ -1382,9 +1399,9 @@ func _inject_local_message(text: String) -> void: _refresh_chat_display() func _trim_old_messages() -> void: - """Remove messages from previous calendar days (daily clear).""" - var today: String = Time.get_date_string_from_system() - _chat_messages = _chat_messages.filter(func(m): return m.get("date", today) == today) + """Keep only the most recent 100 messages to prevent memory/UI bloat.""" + if _chat_messages.size() > 100: + _chat_messages = _chat_messages.slice(-100) func _refresh_chat_display() -> void: """Re-render the RichTextLabel with all buffered messages.""" @@ -1402,20 +1419,28 @@ func _refresh_chat_display() -> void: # Scroll to bottom await get_tree().process_frame - chat_display.scroll_to_line(chat_display.get_line_count()) - -func _format_nakama_time(iso_str: String) -> String: - """Convert Nakama ISO timestamp '2026-04-14T10:30:00Z' → local 'HH:MM'.""" - # Parse the UTC time components - if iso_str.length() < 19: + if chat_display: + var scrollbar = chat_display.get_v_scroll_bar() + if scrollbar: + chat_display.scroll_to_line(chat_display.get_line_count()) + +func _format_nakama_time(time_str: String) -> String: + """Convert Nakama time to local 'HH:MM'.""" + # Nakama returns UNIX epoch as string (e.g. "1714418656") or ISO string. + if time_str.is_valid_int(): + var unix_time = time_str.to_int() + var dict = Time.get_time_dict_from_unix_time(unix_time) + return "%02d:%02d" % [dict.hour, dict.minute] + + # Fallback for ISO strings or empty + if time_str.length() < 19: return _get_local_time_hhmm() - var t_parts = iso_str.split("T") + var t_parts = time_str.split("T") if t_parts.size() < 2: return _get_local_time_hhmm() var time_part = t_parts[1].replace("Z", "").split(":") if time_part.size() < 2: return _get_local_time_hhmm() - # Use UTC hours/minutes directly (simple, avoids TZ complexity in Godot) return "%s:%s" % [time_part[0], time_part[1]] func _get_local_time_hhmm() -> String: commit 8a2f865ad81fb723d8959b2825174288e61a4e46 Author: adtpdn Date: Wed Apr 29 01:36:49 2026 +0800 feat: update 2.1.9 diff --git a/scenes/lobby.gd b/scenes/lobby.gd index bfe615e..5af709b 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -93,8 +93,16 @@ var doors_goals_option: OptionButton @onready var leave_btn = $LobbyPanel/BottomBar/LeaveBtn @onready var ready_btn = $LobbyPanel/BottomBar/ReadyBtn @onready var start_game_btn = $LobbyPanel/BottomBar/StartGameBtn +var invite_btn: Button @onready var status_label = $LobbyPanel/StatusLabel +# Social Panel instance +var social_panel_instance: Control + +# Lobby invite popup +var _invite_popup: AcceptDialog +var _pending_invite_match_id: String = "" + # UI References - Status @onready var connection_status = $StatusBar/ConnectionStatus @@ -175,6 +183,11 @@ func _ready(): if quit_btn: quit_btn.pressed.connect(_on_quit_pressed) + # Social button (main menu) - connect if node exists + var social_btn = get_node_or_null("%SocialBtn") + if social_btn: + social_btn.pressed.connect(_on_social_pressed) + # Connect Server Selection signals if server_option: server_option.item_selected.connect(_on_server_option_selected) @@ -258,6 +271,12 @@ func _ready(): if chat_input: chat_input.text_submitted.connect(func(_t): _on_chat_send_pressed()) + # Connect Social / Friend UI + invite_btn = get_node_or_null("LobbyPanel/BottomBar/InviteBtn") + if invite_btn: + invite_btn.pressed.connect(_on_invite_friends_pressed) + FriendManager.lobby_invite_received.connect(_on_lobby_invite_received) + # Set initial title if already loaded _on_profile_updated() @@ -1075,6 +1094,24 @@ func _update_player_slots() -> void: ready_label.text = "READY ✓" if is_ready else "NOT READY" ready_label.add_theme_color_override("font_color", Color(0.4, 0.8, 0.4) if is_ready else Color(0.6, 0.6, 0.6)) + + # + Friend button — node must exist in lobby.tscn as "AddFriendBtn1", "AddFriendBtn2"... + var player_nakama_id: String = player.get("nakama_id", "") + var my_nakama_id: String = NakamaManager.session.user_id if NakamaManager.session else "" + var add_friend_btn: Button = slot.get_node_or_null("AddFriendBtn%d" % slot_num) + if add_friend_btn: + if player_nakama_id.is_empty() or player_nakama_id == my_nakama_id: + add_friend_btn.visible = false + else: + add_friend_btn.visible = true + if FriendManager.is_friend(player_nakama_id): + add_friend_btn.text = "Friend ✓" + add_friend_btn.disabled = true + else: + add_friend_btn.text = "+ Friend" + add_friend_btn.disabled = false + if not add_friend_btn.pressed.is_connected(func(): _on_add_friend_pressed(player_nakama_id)): + add_friend_btn.pressed.connect(func(): _on_add_friend_pressed(player_nakama_id)) else: # Empty slot - show as bot placeholder slot.visible = true @@ -1112,6 +1149,11 @@ func _update_player_slots() -> void: if ready_label: ready_label.text = "WAITING..." ready_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.7)) + + # Hide friend button for bots/empty slots + var add_friend_btn: Button = slot.get_node_or_null("AddFriendBtn%d" % slot_num) + if add_friend_btn: + add_friend_btn.visible = false _update_status() @@ -1158,6 +1200,85 @@ func _apply_loadout_character() -> void: print("[Lobby] Loadout character applied: ", saved_char) +# ============================================================================= +# Social / Friend Functions +# ============================================================================= + +func _on_add_friend_pressed(nakama_id: String) -> void: + var ok = await FriendManager.add_friend_by_id(nakama_id) + if ok: + _update_player_slots() + +func _on_invite_friends_pressed() -> void: + """Open the invite friends dialog (scene-based).""" + var match_id = current_match_id + if match_id.is_empty(): + return + var friends = FriendManager.get_mutual_friends() + var scene = load("res://scenes/ui/invite_friends_dialog.tscn") as PackedScene + if not scene: + return + var dialog = scene.instantiate() + add_child(dialog) + dialog.open(friends, match_id) + dialog.closed.connect(dialog.queue_free) + +func _on_lobby_invite_received(from_user_id: String, from_name: String, match_id: String) -> void: + """Show invite notification popup. Only shown if not already in a game.""" + if get_tree().current_scene.scene_file_path != "res://scenes/lobby.tscn": + return + if lobby_panel and lobby_panel.visible: + return + + if _invite_popup: + _invite_popup.queue_free() + + _pending_invite_match_id = match_id + var scene = load("res://scenes/ui/lobby_invite_popup.tscn") as PackedScene + if scene: + _invite_popup = scene.instantiate() + add_child(_invite_popup) + _invite_popup.setup(from_name) + _invite_popup.accepted.connect(_on_invite_accepted) + _invite_popup.declined.connect(func(): _invite_popup.queue_free()) + _invite_popup.popup_centered() + else: + # Fallback if scene not yet added to project + var dlg := AcceptDialog.new() + dlg.title = "Lobby Invitation" + dlg.dialog_text = "%s invited you!\nJoin?" % from_name + dlg.ok_button_text = "Join" + dlg.add_cancel_button("Decline") + add_child(dlg) + dlg.confirmed.connect(_on_invite_accepted) + dlg.canceled.connect(dlg.queue_free) + dlg.popup_centered() + _invite_popup = dlg + +func _on_invite_accepted() -> void: + if not _pending_invite_match_id.is_empty(): + LobbyManager.join_room(_pending_invite_match_id) + if _invite_popup: + _invite_popup.queue_free() + _pending_invite_match_id = "" + +func _on_social_pressed() -> void: + """Open social / friend list panel.""" + if not social_panel_instance: + var scene = load("res://scenes/ui/social_panel.tscn") + if scene: + social_panel_instance = scene.instantiate() + social_panel_instance.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(social_panel_instance) + if social_panel_instance.has_signal("closed"): + social_panel_instance.closed.connect(func(): + social_panel_instance.hide() + if main_menu_panel: main_menu_panel.show() + ) + if social_panel_instance: + if main_menu_panel: main_menu_panel.hide() + social_panel_instance.show() + # ============================================================================= # Global Chat System # ============================================================================= commit b76dd2e73771bc8914f85c7bcd7a759cd33d17c4 Author: adtpdn Date: Fri Apr 24 22:56:11 2026 +0800 feat: fix some bug diff --git a/scenes/lobby.gd b/scenes/lobby.gd index fbc3410..bfe615e 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -109,6 +109,7 @@ var admin_panel_instance: Control var current_match_id: String = "" var leaderboard_panel_instance: Control +var shop_panel_instance: Control # Bot name tracking keyed by slot index to avoid re-generating on each update var _bot_names: Dictionary = {} @@ -787,13 +788,16 @@ func _on_shop_pressed() -> void: connection_status.text = "Must be logged in" return - var shop_scene = load("res://scenes/ui/shop_panel.tscn") - if shop_scene: - var shop = shop_scene.instantiate() - add_child(shop) + if not shop_panel_instance: + var shop_scene = load("res://scenes/ui/shop_panel.tscn") + if shop_scene: + shop_panel_instance = shop_scene.instantiate() + add_child(shop_panel_instance) + shop_panel_instance.closed.connect(func(): if main_menu_panel: main_menu_panel.show()) + + if shop_panel_instance: if main_menu_panel: main_menu_panel.hide() - shop.closed.connect(func(): if main_menu_panel: main_menu_panel.show()) - shop.show_panel() + shop_panel_instance.show_panel() func _on_banner1_pressed() -> void: var gacha_scene = load("res://scenes/ui/gacha_panel.tscn") commit 5a08db38de7c70b21fc1507246fc1e8ecbd8f78e Author: adtpdn Date: Fri Apr 24 02:50:16 2026 +0800 feat: fix gatcha, and login flow connection diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 9a929a4..fbc3410 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -29,6 +29,7 @@ extends Control @onready var leaderboard_btn = %LeaderboardBtn @onready var shop_btn = %CartBtn @onready var top_right_profile_btn = %ProfileBtn +@onready var banner1_btn = %Banner1 # UI References - Room List @onready var room_list_panel = $RoomListPanel @@ -161,6 +162,12 @@ func _ready(): # Shop Button if shop_btn: shop_btn.pressed.connect(_on_shop_pressed) + + # Banner1 → Gacha + if banner1_btn: + banner1_btn.disabled = false + banner1_btn.text = "✨ Gacha" + banner1_btn.pressed.connect(_on_banner1_pressed) if leaderboard_btn: leaderboard_btn.pressed.connect(_on_leaderboard_pressed) @@ -788,6 +795,31 @@ func _on_shop_pressed() -> void: shop.closed.connect(func(): if main_menu_panel: main_menu_panel.show()) shop.show_panel() +func _on_banner1_pressed() -> void: + var gacha_scene = load("res://scenes/ui/gacha_panel.tscn") + if not gacha_scene: + connection_status.text = "Gacha panel not found" + return + var gacha = gacha_scene.instantiate() + gacha.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(gacha) + if main_menu_panel: main_menu_panel.hide() + # GachaPanel emits "closed" from its BackBtn handler + if gacha.has_signal("closed"): + gacha.closed.connect(func(): + gacha.queue_free() + if main_menu_panel: main_menu_panel.show() + ) + else: + # Fallback: listen for BackBtn directly + var back = gacha.get_node_or_null("%BackBtn") + if back: + back.pressed.connect(func(): + gacha.queue_free() + if main_menu_panel: main_menu_panel.show() + ) + + func _on_leaderboard_pressed() -> void: if not leaderboard_panel_instance: var leaderboard_panel_scene := load("res://scenes/ui/leaderboard_panel.tscn") commit f2e14f20f306040a41f9b7e4da1148d69344d40b Author: adtpdn Date: Mon Apr 20 19:32:52 2026 +0800 feat: skin update diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 207c497..9a929a4 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -296,6 +296,9 @@ func _setup_3d_preview() -> void: anim_player.play("animation-pack/idle") elif anim_player.get_animation_list().size() > 0: anim_player.play(anim_player.get_animation_list()[0]) + + # Apply equipped cosmetics so the lobby preview shows the current loadout + SkinManager.apply_loadout(character_root, UserProfileManager.loadout) func _load_character_textures() -> void: """Load character preview textures.""" commit f10d777c90b7f2c293633328aaf6256bb71c658f Author: adtpdn Date: Wed Apr 15 16:26:49 2026 +0800 feat: overhaul UI main and profile diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 73de880..207c497 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -2,22 +2,33 @@ extends Control # UI References - Main Menu @onready var main_menu_panel = $MainMenuPanel -@onready var main_title = $MainMenuPanel/HiddenLogic/Title -@onready var username_label = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/ProfileBox/VBoxContainer/Username -@onready var main_subtitle = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/ProfileBox/VBoxContainer/Subtitle -@onready var create_room_btn = $MainMenuPanel/MainContainer/MarginContainer/BottomRightPanel/CreateRoomBtn -@onready var browse_rooms_btn = $MainMenuPanel/MainContainer/MarginContainer/BottomRightPanel/BrowseRoomsBtn -@onready var tutorial_btn = $MainMenuPanel/MainContainer/MarginContainer/BottomRightPanel/TutorialBtn -@onready var main_menu_profile_btn = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/ProfileBox/ProfileBtn -@onready var lobby_settings_btn = $MainMenuPanel/MainContainer/MarginContainer/TopRightPanel/SettingsBtn -@onready var quit_btn = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/HBoxContainer/QuitBtn +@onready var main_title = %Title +@onready var username_label = %Username +@onready var main_subtitle = %Subtitle +@onready var create_room_btn = %CreateRoomBtn +@onready var browse_rooms_btn = %BrowseRoomsBtn +@onready var tutorial_btn = %TutorialBtn +@onready var main_menu_profile_btn = %MainProfileBtn +@onready var avatar_display = %AvatarDisplay +@onready var lobby_settings_btn = %SettingsBtn +@onready var quit_btn = %QuitBtn + +# Main Menu 3D Preview +@onready var character_root = %CharacterRoot +@onready var anim_player = %AnimationPlayer + +# UI References - Currencies +@onready var gold_label = %GoldLabel +@onready var star_label = %StarLabel # UI References - Server Selection -@onready var server_option = $MainMenuPanel/HiddenLogic/ServerOption -@onready var server_ip_input = $MainMenuPanel/HiddenLogic/ServerIPInput +@onready var server_option = %ServerOption +@onready var server_ip_input = %ServerIPInput # Leaderboard Reference -@onready var leaderboard_btn = $MainMenuPanel/MainContainer/MarginContainer/TopRightPanel/LeaderboardBtn +@onready var leaderboard_btn = %LeaderboardBtn +@onready var shop_btn = %CartBtn +@onready var top_right_profile_btn = %ProfileBtn # UI References - Room List @onready var room_list_panel = $RoomListPanel @@ -108,9 +119,9 @@ const GLOBAL_CHAT_ROOM := "global_lobby" var _chat_channel = null var _chat_messages: Array = [] -@onready var chat_display: RichTextLabel = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/ChatPanel/MarginContainer/RichTextLabel -@onready var chat_input: LineEdit = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/HBoxContainer/ChatInput -@onready var chat_send_btn: Button = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/HBoxContainer/SendBtn +@onready var chat_display: RichTextLabel = %RichTextLabel +@onready var chat_input: LineEdit = %ChatInput +@onready var chat_send_btn: Button = %SendBtn # Server Selection Controls (Now in tscn) # var server_option: OptionButton @@ -142,8 +153,15 @@ func _ready(): tutorial_btn.pressed.connect(_on_tutorial_pressed) if main_menu_profile_btn: main_menu_profile_btn.pressed.connect(_on_profile_btn_pressed) + if top_right_profile_btn: + top_right_profile_btn.pressed.connect(_on_profile_btn_pressed) if lobby_settings_btn: lobby_settings_btn.pressed.connect(_on_settings_pressed) + + # Shop Button + if shop_btn: + shop_btn.pressed.connect(_on_shop_pressed) + if leaderboard_btn: leaderboard_btn.pressed.connect(_on_leaderboard_pressed) if quit_btn: @@ -252,6 +270,33 @@ func _ready(): # Setup # ============================================================================= +const CHAR_NODE_MAP: Dictionary = { + "Copper": "Oldpop", + "Dabro": "Masbro", + "Gatot": "Gatot", + "Pip": "Bob" +} + +func _setup_3d_preview() -> void: + """Swaps out the active character inside the 3D MainMenu SubViewport.""" + if not character_root: + return + var target_character = UserProfileManager.profile.get("loadout_character", "Copper") + var node_name = CHAR_NODE_MAP.get(target_character, "Bob") + + for child in character_root.get_children(): + if child is Node3D: + child.visible = (child.name == node_name) + + if anim_player: + var new_root: Node3D = character_root.get_node_or_null(node_name) as Node3D + if new_root: + anim_player.root_node = new_root.get_path() + if anim_player.has_animation("animation-pack/idle"): + anim_player.play("animation-pack/idle") + elif anim_player.get_animation_list().size() > 0: + anim_player.play(anim_player.get_animation_list()[0]) + func _load_character_textures() -> void: """Load character preview textures.""" var characters = { @@ -672,12 +717,19 @@ func _on_profile_btn_pressed() -> void: if not profile_panel_instance: var profile_panel_scene := load("res://scenes/ui/profile_panel.tscn") profile_panel_instance = profile_panel_scene.instantiate() - profile_panel_instance.closed.connect(func(): profile_panel_instance.hide()) + profile_panel_instance.closed.connect(func(): + profile_panel_instance.hide() + if main_menu_panel: + main_menu_panel.show() + ) # Full-screen overlay — fill the entire lobby viewport profile_panel_instance.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(profile_panel_instance) - profile_panel_instance.show_panel() + if profile_panel_instance: + if main_menu_panel: + main_menu_panel.hide() + profile_panel_instance.show_panel() func _on_logout_pressed() -> void: AuthManager.logout() @@ -696,26 +748,60 @@ func _on_settings_pressed(): settings_menu.name = "SettingsMenu" add_child(settings_menu) - # Connect close button + # Connect close button — restore whichever panel was visible before var close_btn = settings_menu.get_node_or_null("PanelContainer/VBoxContainer/Header/CloseButton") if close_btn: - # settings_menu.gd handles basic visibility, but we can override or add to it - pass + close_btn.pressed.connect(func(): + if lobby_panel and lobby_panel.visible: + pass # lobby_panel restored below via _restore_panel + _restore_after_settings() + ) if settings_menu: + # Remember what is currently active + settings_menu.set_meta("from_lobby", lobby_panel.visible if lobby_panel else false) + main_menu_panel.hide() + if lobby_panel: lobby_panel.hide() settings_menu.open() +func _restore_after_settings() -> void: + var settings_menu = get_node_or_null("SettingsMenu") + var from_lobby: bool = settings_menu.get_meta("from_lobby", false) if settings_menu else false + if from_lobby: + if lobby_panel: lobby_panel.show() + else: + if main_menu_panel: main_menu_panel.show() + +func _on_shop_pressed() -> void: + if not NakamaManager.session: + connection_status.text = "Must be logged in" + return + + var shop_scene = load("res://scenes/ui/shop_panel.tscn") + if shop_scene: + var shop = shop_scene.instantiate() + add_child(shop) + if main_menu_panel: main_menu_panel.hide() + shop.closed.connect(func(): if main_menu_panel: main_menu_panel.show()) + shop.show_panel() + func _on_leaderboard_pressed() -> void: if not leaderboard_panel_instance: var leaderboard_panel_scene := load("res://scenes/ui/leaderboard_panel.tscn") if leaderboard_panel_scene: leaderboard_panel_instance = leaderboard_panel_scene.instantiate() - leaderboard_panel_instance.closed.connect(func(): leaderboard_panel_instance.hide()) + leaderboard_panel_instance.closed.connect(func(): + leaderboard_panel_instance.hide() + if main_menu_panel: + main_menu_panel.show() + ) # Full-screen overlay leaderboard_panel_instance.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(leaderboard_panel_instance) if leaderboard_panel_instance: + if main_menu_panel: + main_menu_panel.hide() leaderboard_panel_instance.show_panel() func _go_to_login() -> void: @@ -873,15 +959,22 @@ func _on_profile_updated() -> void: if username_label: username_label.text = display_name - if main_menu_profile_btn: + if avatar_display: var avatar_url = UserProfileManager.get_avatar_url() if ResourceLoader.exists(avatar_url): - main_menu_profile_btn.icon = load(avatar_url) - main_menu_profile_btn.expand_icon = true - main_menu_profile_btn.text = "" + avatar_display.texture = load(avatar_url) + if main_menu_profile_btn: + main_menu_profile_btn.text = "" + + if gold_label and UserProfileManager.is_profile_loaded: + gold_label.text = str(UserProfileManager.wallet.get("gold", 0)) + if star_label and UserProfileManager.is_profile_loaded: + star_label.text = str(UserProfileManager.wallet.get("star", 0)) # Sync to LobbyManager LobbyManager.set_player_name(display_name) + + _setup_3d_preview() # ============================================================================= # Player Slot Updates commit 01661a56ba3a1adca62193b0fc7959077ad2a987 Author: adtpdn Date: Wed Apr 15 07:31:49 2026 +0800 feat: update chat diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 9047994..73de880 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -2,20 +2,22 @@ extends Control # UI References - Main Menu @onready var main_menu_panel = $MainMenuPanel -@onready var main_title = $MainMenuPanel/VBoxContainer/TitleContainer/Title -@onready var main_subtitle = $MainMenuPanel/VBoxContainer/TitleContainer/Subtitle -@onready var create_room_btn = $MainMenuPanel/VBoxContainer/ButtonSection/CreateRoomBtn -@onready var browse_rooms_btn = $MainMenuPanel/VBoxContainer/ButtonSection/BrowseRoomsBtn -@onready var main_menu_profile_btn = $MainMenuPanel/VBoxContainer/ButtonSection/ProfileBtn -@onready var lobby_settings_btn = $MainMenuPanel/VBoxContainer/ButtonSection/SettingsBtn -@onready var quit_btn = $MainMenuPanel/VBoxContainer/ButtonSection/QuitBtn +@onready var main_title = $MainMenuPanel/HiddenLogic/Title +@onready var username_label = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/ProfileBox/VBoxContainer/Username +@onready var main_subtitle = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/ProfileBox/VBoxContainer/Subtitle +@onready var create_room_btn = $MainMenuPanel/MainContainer/MarginContainer/BottomRightPanel/CreateRoomBtn +@onready var browse_rooms_btn = $MainMenuPanel/MainContainer/MarginContainer/BottomRightPanel/BrowseRoomsBtn +@onready var tutorial_btn = $MainMenuPanel/MainContainer/MarginContainer/BottomRightPanel/TutorialBtn +@onready var main_menu_profile_btn = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/ProfileBox/ProfileBtn +@onready var lobby_settings_btn = $MainMenuPanel/MainContainer/MarginContainer/TopRightPanel/SettingsBtn +@onready var quit_btn = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/HBoxContainer/QuitBtn # UI References - Server Selection -@onready var server_option = $MainMenuPanel/VBoxContainer/ServerSelectionSection/ServerOption -@onready var server_ip_input = $MainMenuPanel/VBoxContainer/ServerSelectionSection/ServerIPInput +@onready var server_option = $MainMenuPanel/HiddenLogic/ServerOption +@onready var server_ip_input = $MainMenuPanel/HiddenLogic/ServerIPInput # Leaderboard Reference -@onready var leaderboard_btn = $MainMenuPanel/VBoxContainer/ButtonSection/LeaderboardBtn +@onready var leaderboard_btn = $MainMenuPanel/MainContainer/MarginContainer/TopRightPanel/LeaderboardBtn # UI References - Room List @onready var room_list_panel = $RoomListPanel @@ -99,10 +101,22 @@ var leaderboard_panel_instance: Control # Bot name tracking keyed by slot index to avoid re-generating on each update var _bot_names: Dictionary = {} +# ============================================================================= +# Chat System +# ============================================================================= +const GLOBAL_CHAT_ROOM := "global_lobby" +var _chat_channel = null +var _chat_messages: Array = [] + +@onready var chat_display: RichTextLabel = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/ChatPanel/MarginContainer/RichTextLabel +@onready var chat_input: LineEdit = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/HBoxContainer/ChatInput +@onready var chat_send_btn: Button = $MainMenuPanel/MainContainer/MarginContainer/LeftPanel/HBoxContainer/SendBtn + # Server Selection Controls (Now in tscn) # var server_option: OptionButton # var server_ip_input: LineEdit + func _ready(): # Start background music for Lobby MusicManager.start_music() @@ -121,21 +135,11 @@ func _ready(): # Initial UI update _on_profile_updated() - # Inject Tutorial button - var tutorial_btn = Button.new() - tutorial_btn.name = "TutorialBtn" - tutorial_btn.text = "PLAY TUTORIAL" - tutorial_btn.theme_type_variation = "FlatButton" - tutorial_btn.add_theme_font_size_override("font_size", 24) - tutorial_btn.pressed.connect(_on_tutorial_pressed) - var btn_section = main_menu_panel.get_node_or_null("VBoxContainer/ButtonSection") - if btn_section: - btn_section.add_child(tutorial_btn) - btn_section.move_child(tutorial_btn, 0) - # Connect button signals - Main Menu create_room_btn.pressed.connect(_on_create_room_pressed) browse_rooms_btn.pressed.connect(_on_browse_rooms_pressed) + if tutorial_btn: + tutorial_btn.pressed.connect(_on_tutorial_pressed) if main_menu_profile_btn: main_menu_profile_btn.pressed.connect(_on_profile_btn_pressed) if lobby_settings_btn: @@ -222,6 +226,12 @@ func _ready(): UserProfileManager.profile_loaded.connect(func(_p): _on_profile_updated()) UserProfileManager.profile_updated.connect(_on_profile_updated) + # Connect Chat UI + if chat_send_btn: + chat_send_btn.pressed.connect(_on_chat_send_pressed) + if chat_input: + chat_input.text_submitted.connect(func(_t): _on_chat_send_pressed()) + # Set initial title if already loaded _on_profile_updated() @@ -232,6 +242,11 @@ func _ready(): if not LobbyManager.disconnect_reason.is_empty(): connection_status.text = LobbyManager.disconnect_reason LobbyManager.disconnect_reason = "" + + # Try to join global chat if already connected + if NakamaManager.is_connected_to_nakama(): + _join_global_chat() + # ============================================================================= # Setup @@ -831,6 +846,7 @@ func _on_area_changed(area_name: String) -> void: func _on_connected_to_nakama() -> void: connection_status.text = "Connected to server" + _join_global_chat() func _on_connection_failed(error_message: String) -> void: connection_status.text = "Connection failed: %s" % error_message @@ -854,6 +870,16 @@ func _on_profile_updated() -> void: if main_title: main_title.text = display_name + if username_label: + username_label.text = display_name + + if main_menu_profile_btn: + var avatar_url = UserProfileManager.get_avatar_url() + if ResourceLoader.exists(avatar_url): + main_menu_profile_btn.icon = load(avatar_url) + main_menu_profile_btn.expand_icon = true + main_menu_profile_btn.text = "" + # Sync to LobbyManager LobbyManager.set_player_name(display_name) @@ -998,3 +1024,155 @@ func _apply_loadout_character() -> void: if idx != -1: LobbyManager.local_character_index = idx print("[Lobby] Loadout character applied: ", saved_char) + + +# ============================================================================= +# Global Chat System +# ============================================================================= + +func _join_global_chat() -> void: + """Join the persistent global lobby chat channel via Nakama socket.""" + # Already in the channel — don't rejoin + if _chat_channel != null: + return + + var socket = NakamaManager.socket + if not socket or not socket.is_connected_to_host(): + push_warning("[Chat] Socket not connected, skipping chat join.") + return + + # Join a persistent room channel named by our constant + var result = await socket.join_chat_async(GLOBAL_CHAT_ROOM, + NakamaSocket.ChannelType.Room, + true, # persistence = true (server stores history) + false) # hidden = false + + if result.is_exception(): + push_warning("[Chat] Failed to join global chat: " + result.get_exception().message) + return + + _chat_channel = result + print("[Chat] Joined global channel: ", _chat_channel.id) + + # Connect incoming message signal + if not socket.received_channel_message.is_connected(_on_chat_message_received): + socket.received_channel_message.connect(_on_chat_message_received) + + # Load history and render (Nakama sends recent messages on join via received_channel_message) + _refresh_chat_display() + +func _on_chat_message_received(message) -> void: + """Nakama socket signal: a message arrived on any channel.""" + # message is ApiChannelMessage — use direct property access, NOT .get() + if _chat_channel == null or message.channel_id != _chat_channel.id: + return + + # content is a String (JSON) — parse to extract our "msg" field + var text: String = "" + var parsed = JSON.parse_string(message.content) + if typeof(parsed) == TYPE_DICTIONARY: + text = parsed.get("msg", message.content) + else: + text = message.content + + # Sender: use username property directly (falls back to first 8 chars of sender_id) + var sender: String = message.username + if sender.is_empty(): + sender = message.sender_id.substr(0, 8) + + # Timestamp → HH:MM + var ts_str: String = _format_nakama_time(message.create_time) + + _chat_messages.append({ + "sender": sender, + "content": text, + "ts": ts_str, + "date": message.create_time.substr(0, 10) + }) + + _trim_old_messages() + _refresh_chat_display() + +func _on_chat_send_pressed() -> void: + """Send a message to the global chat channel.""" + if chat_input == null or chat_input.text.strip_edges().is_empty(): + return + + var text = chat_input.text.strip_edges() + chat_input.text = "" + chat_input.grab_focus() + + var socket = NakamaManager.socket + if not socket or _chat_channel == null: + # Offline fallback: show locally only + _inject_local_message(text) + return + + # Nakama GDScript SDK: write_chat_message_async takes a Dictionary, not a JSON string + var content := {"msg": text} + var result = await socket.write_chat_message_async(_chat_channel.id, content) + if result.is_exception(): + push_warning("[Chat] Failed to send message: " + result.get_exception().message) + # Still show it locally + _inject_local_message(text) + +func _inject_local_message(text: String) -> void: + """Display a message as the local player when offline/fallback.""" + var display_name = UserProfileManager.get_display_name("You") + var ts_str = _get_local_time_hhmm() + _chat_messages.append({ + "sender": display_name, + "content": text, + "ts": ts_str, + "date": Time.get_date_string_from_system() + }) + _refresh_chat_display() + +func _trim_old_messages() -> void: + """Remove messages from previous calendar days (daily clear).""" + var today: String = Time.get_date_string_from_system() + _chat_messages = _chat_messages.filter(func(m): return m.get("date", today) == today) + +func _refresh_chat_display() -> void: + """Re-render the RichTextLabel with all buffered messages.""" + if not chat_display: + return + + chat_display.clear() + for msg in _chat_messages: + var ts: String = msg.get("ts", "") + var sender: String = msg.get("sender", "?") + var text: String = msg.get("content", "") + + # Format: [dim][HH:MM] [/dim][b]Name:[/b] message + chat_display.append_text("[color=#888888][%s][/color] [b]%s:[/b] %s\n" % [ts, sender, text]) + + # Scroll to bottom + await get_tree().process_frame + chat_display.scroll_to_line(chat_display.get_line_count()) + +func _format_nakama_time(iso_str: String) -> String: + """Convert Nakama ISO timestamp '2026-04-14T10:30:00Z' → local 'HH:MM'.""" + # Parse the UTC time components + if iso_str.length() < 19: + return _get_local_time_hhmm() + var t_parts = iso_str.split("T") + if t_parts.size() < 2: + return _get_local_time_hhmm() + var time_part = t_parts[1].replace("Z", "").split(":") + if time_part.size() < 2: + return _get_local_time_hhmm() + # Use UTC hours/minutes directly (simple, avoids TZ complexity in Godot) + return "%s:%s" % [time_part[0], time_part[1]] + +func _get_local_time_hhmm() -> String: + var t = Time.get_time_dict_from_system() + return "%02d:%02d" % [t.hour, t.minute] + +func _leave_global_chat() -> void: + """Leave the chat channel cleanly.""" + var socket = NakamaManager.socket + if socket and _chat_channel: + socket.received_channel_message.disconnect(_on_chat_message_received) + await socket.leave_chat_async(_chat_channel.id) + _chat_channel = null commit 5323aea5fb7788283e93dab0d10d01e36d72a52a Author: adtpdn Date: Fri Apr 10 05:17:30 2026 +0800 feat: add tutorial section diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 2f9ee61..9047994 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -121,6 +121,18 @@ func _ready(): # Initial UI update _on_profile_updated() + # Inject Tutorial button + var tutorial_btn = Button.new() + tutorial_btn.name = "TutorialBtn" + tutorial_btn.text = "PLAY TUTORIAL" + tutorial_btn.theme_type_variation = "FlatButton" + tutorial_btn.add_theme_font_size_override("font_size", 24) + tutorial_btn.pressed.connect(_on_tutorial_pressed) + var btn_section = main_menu_panel.get_node_or_null("VBoxContainer/ButtonSection") + if btn_section: + btn_section.add_child(tutorial_btn) + btn_section.move_child(tutorial_btn, 0) + # Connect button signals - Main Menu create_room_btn.pressed.connect(_on_create_room_pressed) browse_rooms_btn.pressed.connect(_on_browse_rooms_pressed) @@ -333,6 +345,21 @@ func _show_panel(panel_name: String) -> void: # Main Menu Button Handlers # ============================================================================= +func _on_tutorial_pressed() -> void: + if AuthManager.is_guest: + if LobbyManager.local_player_name.is_empty() or LobbyManager.local_player_name == "Player": + LobbyManager.local_player_name = NameGenerator.generate_guest_name() + else: + LobbyManager.local_player_name = UserProfileManager.get_display_name() + connection_status.text = "Starting Tutorial Scenario..." + _apply_loadout_character() + LobbyManager.start_tutorial("Freemode") + # Give the engine two frames for room_joined signal to settle, + # then immediately force-start the game — bypassing the lobby wait screen. + await get_tree().process_frame + await get_tree().process_frame + LobbyManager.start_game(true) + func _on_create_room_pressed() -> void: # Use profile name for logged-in users, or guest for others if AuthManager.is_guest: commit 733b1da8e0190a6d1ba74fa666a2add31409a9be Author: adtpdn Date: Fri Apr 3 05:11:49 2026 +0800 feat: edit some login indicator diff --git a/scenes/lobby.gd b/scenes/lobby.gd index fde2678..2f9ee61 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -2,6 +2,8 @@ extends Control # UI References - Main Menu @onready var main_menu_panel = $MainMenuPanel +@onready var main_title = $MainMenuPanel/VBoxContainer/TitleContainer/Title +@onready var main_subtitle = $MainMenuPanel/VBoxContainer/TitleContainer/Subtitle @onready var create_room_btn = $MainMenuPanel/VBoxContainer/ButtonSection/CreateRoomBtn @onready var browse_rooms_btn = $MainMenuPanel/VBoxContainer/ButtonSection/BrowseRoomsBtn @onready var main_menu_profile_btn = $MainMenuPanel/VBoxContainer/ButtonSection/ProfileBtn @@ -116,11 +118,8 @@ func _ready(): # Setup Game Mode specific UI dynamically _create_custom_settings_ui() - # Set player name from profile - if AuthManager.is_guest: - LobbyManager.local_player_name = NameGenerator.generate_guest_name() - else: - LobbyManager.local_player_name = UserProfileManager.get_display_name() + # Initial UI update + _on_profile_updated() # Connect button signals - Main Menu create_room_btn.pressed.connect(_on_create_room_pressed) @@ -208,8 +207,12 @@ func _ready(): NakamaManager.connection_failed.connect(_on_connection_failed) # Connect UserProfileManager signals + UserProfileManager.profile_loaded.connect(func(_p): _on_profile_updated()) UserProfileManager.profile_updated.connect(_on_profile_updated) + # Set initial title if already loaded + _on_profile_updated() + # Show main menu initially _show_panel("main_menu") @@ -239,6 +242,9 @@ func _load_character_textures() -> void: print("[Lobby] Character texture not found: ", tex_path) func _on_server_option_selected(index: int) -> void: + if main_subtitle and server_option: + main_subtitle.text = server_option.get_item_text(index).to_upper() + if index == 0: # Nakama Localhost if server_ip_input: server_ip_input.visible = false @@ -805,10 +811,24 @@ func _on_connection_failed(error_message: String) -> void: func _on_profile_updated() -> void: """Handle profile updates (name/avatar change).""" - var new_name = UserProfileManager.get_display_name() + var display_name: String = "" + + if UserProfileManager.is_profile_loaded: + display_name = UserProfileManager.get_display_name() + elif not AuthManager.is_guest and AuthManager.is_authenticated: + display_name = "LOADING..." + else: + # Is Guest or not logged in yet + if LobbyManager.local_player_name.is_empty() or LobbyManager.local_player_name == "Guest": + display_name = NameGenerator.generate_guest_name() + else: + display_name = LobbyManager.local_player_name + + if main_title: + main_title.text = display_name - # Sync to LobbyManager if we are in a room or just locally - LobbyManager.set_player_name(new_name) + # Sync to LobbyManager + LobbyManager.set_player_name(display_name) # ============================================================================= # Player Slot Updates commit 1c4d3170ef2767855f2a7e0a7c261d38e4faa3b5 Author: Yogi Wiguna Date: Tue Mar 31 21:51:45 2026 +0800 feat: implement MusicManager singleton and integrate background music into lobby scene diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 773a5d9..fde2678 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -102,8 +102,8 @@ var _bot_names: Dictionary = {} # var server_ip_input: LineEdit func _ready(): - # Stop background music when in Lobby - MusicManager.stop_music() + # Start background music for Lobby + MusicManager.start_music() # Load character textures _load_character_textures() commit cc19c7852d56d8463b7c74aabd7da30cb2e68fa8 Author: Yogi Wiguna Date: Tue Mar 31 21:36:26 2026 +0800 feat: implement settings menu, audio management system, and lobby UI framework diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 426a637..773a5d9 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -102,10 +102,9 @@ var _bot_names: Dictionary = {} # var server_ip_input: LineEdit func _ready(): - # Check if user is authenticated (Commented out to allow server config on main menu) - # if not AuthManager.is_logged_in(): - # call_deferred("_go_to_login") - # return + # Stop background music when in Lobby + MusicManager.stop_music() + # Load character textures _load_character_textures() commit a916a57c05c98fdf085d82778592e2d5f23d3b85 Author: adtpdn Date: Fri Mar 27 22:18:29 2026 +0800 feat: updated UI UX for profile and leaderboard menu diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 68cf7cc..426a637 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -94,6 +94,9 @@ var current_match_id: String = "" var leaderboard_panel_instance: Control +# Bot name tracking keyed by slot index to avoid re-generating on each update +var _bot_names: Dictionary = {} + # Server Selection Controls (Now in tscn) # var server_option: OptionButton # var server_ip_input: LineEdit @@ -116,7 +119,7 @@ func _ready(): # Set player name from profile if AuthManager.is_guest: - LobbyManager.local_player_name = "Guest" + LobbyManager.local_player_name = NameGenerator.generate_guest_name() else: LobbyManager.local_player_name = UserProfileManager.get_display_name() @@ -329,17 +332,21 @@ func _on_create_room_pressed() -> void: # Use profile name for logged-in users, or guest for others if AuthManager.is_guest: if LobbyManager.local_player_name.is_empty() or LobbyManager.local_player_name == "Player": - LobbyManager.local_player_name = "Guest" + LobbyManager.local_player_name = NameGenerator.generate_guest_name() else: LobbyManager.local_player_name = UserProfileManager.get_display_name() if LobbyManager.is_lan_mode: connection_status.text = "Starting LAN room..." + # Apply loadout character before creating room + _apply_loadout_character() var ok = await LobbyManager.create_room_lan("LAN Room " + str(randi_range(100, 999))) if not ok: connection_status.text = "Failed to start LAN room. Check port 7777." else: connection_status.text = "Creating Nakama room..." + # Apply loadout character before creating room + _apply_loadout_character() LobbyManager.create_room("Room %d" % randi_range(1000, 9999)) func _on_browse_rooms_pressed() -> void: @@ -399,7 +406,7 @@ func _on_join_pressed() -> void: # Determine player name if AuthManager.is_guest: if LobbyManager.local_player_name.is_empty(): - LobbyManager.local_player_name = "Guest" + LobbyManager.local_player_name = NameGenerator.generate_guest_name() else: LobbyManager.local_player_name = UserProfileManager.get_display_name() @@ -417,6 +424,8 @@ func _on_join_pressed() -> void: return connection_status.text = "Connecting to %s..." % match_id + # Apply loadout character before joining + _apply_loadout_character() var ok = LobbyManager.join_room_lan(match_id) if not ok: connection_status.text = "Failed to connect to %s" % match_id @@ -425,6 +434,8 @@ func _on_join_pressed() -> void: connection_status.text = "No room selected" return connection_status.text = "Joining Nakama room..." + # Apply loadout character before joining + _apply_loadout_character() LobbyManager.join_room(match_id) func _on_back_pressed() -> void: @@ -529,6 +540,10 @@ func _on_leave_pressed() -> void: _show_panel("main_menu") ready_btn.button_pressed = false ready_btn.text = "READY" + # Release bot names so they can be reused in the next session + for key in _bot_names.keys(): + NameGenerator.release_bot_name(_bot_names[key]) + _bot_names.clear() func _on_copy_id_pressed() -> void: DisplayServer.clipboard_set(current_match_id) @@ -611,10 +626,11 @@ func _on_profile_btn_pressed() -> void: var profile_panel_scene := load("res://scenes/ui/profile_panel.tscn") profile_panel_instance = profile_panel_scene.instantiate() profile_panel_instance.closed.connect(func(): profile_panel_instance.hide()) + # Full-screen overlay — fill the entire lobby viewport + profile_panel_instance.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(profile_panel_instance) profile_panel_instance.show_panel() - profile_panel_instance.position = (get_viewport_rect().size - profile_panel_instance.size) / 2 func _on_logout_pressed() -> void: AuthManager.logout() @@ -648,11 +664,12 @@ func _on_leaderboard_pressed() -> void: if leaderboard_panel_scene: leaderboard_panel_instance = leaderboard_panel_scene.instantiate() leaderboard_panel_instance.closed.connect(func(): leaderboard_panel_instance.hide()) + # Full-screen overlay + leaderboard_panel_instance.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(leaderboard_panel_instance) if leaderboard_panel_instance: leaderboard_panel_instance.show_panel() - # Center it and apply some offset if needed func _go_to_login() -> void: if get_tree(): @@ -716,6 +733,10 @@ func _on_room_joined(room_data: Dictionary) -> void: func _on_room_left() -> void: _show_panel("main_menu") connection_status.text = "Left room" + # Release bot names for reuse + for key in _bot_names.keys(): + NameGenerator.release_bot_name(_bot_names[key]) + _bot_names.clear() func _on_host_disconnected() -> void: # Keep the connection status updated in the UI @@ -854,10 +875,15 @@ func _update_player_slots() -> void: # Empty slot - show as bot placeholder slot.visible = true - # Update name to show as bot + # Assign stable bot name for this slot (generate once, reuse) + if not _bot_names.has(i): + _bot_names[i] = NameGenerator.generate_bot_name() + var bot_display_name: String = _bot_names[i] + + # Update name to show bot with an icon prefix var name_label = slot.get_node_or_null("PlayerName%d" % slot_num) if name_label: - name_label.text = "🤖 Bot %d" % slot_num + name_label.text = "🤖 " + bot_display_name # Use a character for bot preview var char_preview = slot.get_node_or_null("CharacterPreview%d" % slot_num) @@ -916,3 +942,13 @@ func _truncate_id(id: String) -> String: if id.length() > 16: return id.substr(0, 8) + "..." + id.substr(-4) return id + +func _apply_loadout_character() -> void: + """Apply the player's saved loadout default character to LobbyManager before entering a room.""" + var saved_char: String = UserProfileManager.profile.get("loadout_character", "") + if saved_char.is_empty(): + return + var idx := LobbyManager.available_characters.find(saved_char) + if idx != -1: + LobbyManager.local_character_index = idx + print("[Lobby] Loadout character applied: ", saved_char)