feat: 2.3.1
This commit is contained in:
+542
-52
@@ -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])
|
||||
# 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()
|
||||
|
||||
if rooms.size() == 0:
|
||||
connection_status.text = "No rooms available"
|
||||
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]]
|
||||
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_hhmm() -> String:
|
||||
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."""
|
||||
|
||||
Reference in New Issue
Block a user