decdb74ade
Bump export_presets.cfg version to 2.3.5. Update CHANGELOG_DRAFT.md. Refactor lobby.gd into LobbyChat, LobbyMainMenu, LobbyRoomList, LobbyRoom. Move Nakama config to environment variables in nakama_manager.gd. Derive auth_manager.gd encryption key from OS.get_unique_id().sha256_text(). Remove Steam email auth fallback. Require auth ticket. Make GachaManager.pull() async in gacha_panel.gd. Remove dummy wallet seeding. Add store_type to IAP payload. Validate IAP receipts server-side in economy.lua. Register gacha module in main.lua. Clean backend_service.gd stubs. Fix featured_banners type safety in gacha_manager.gd. Guards non-array responses. Move tiles_armagedon_a1.res to assets/models/meshes/. Fix import fallback_path.
2259 lines
172 KiB
Plaintext
2259 lines
172 KiB
Plaintext
commit 7ca11c6534122abd45ff0505d057fd1f23b18645
|
|
Author: adtpdn <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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 <yogiwigunand@gmail.com>
|
|
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 <yogiwigunand@gmail.com>
|
|
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 <adtpdn@gmail.com>
|
|
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)
|