feat: 2.3.1

This commit is contained in:
2026-05-11 17:24:47 +08:00
parent 57e56412e0
commit 13f3c3d591
733 changed files with 17957 additions and 798 deletions
+79
View File
@@ -36,6 +36,17 @@ signal closed
var _daily_reward_config_data: Dictionary = {}
var _current_dr_month: String = ""
# Tab: Announcements
@onready var target_user_edit := %TargetUserEdit as LineEdit
@onready var title_edit := %TitleEdit as LineEdit
@onready var content_edit := %ContentEdit as TextEdit
@onready var start_date_edit := %StartDatePicker as Button
@onready var end_date_edit := %EndDatePicker as Button
@onready var add_reward_btn := %AddRewardBtn as Button
@onready var rewards_list := %RewardsList as VBoxContainer
@onready var reward_row_template := %RewardRowTemplate as HBoxContainer
@onready var send_mail_btn := %SendMailBtn as Button
const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
# -- Data --
@@ -151,6 +162,10 @@ func _connect_signals() -> void:
load_dr_btn.pressed.connect(_load_daily_rewards_config)
save_dr_btn.pressed.connect(_save_daily_rewards_config)
month_option_btn.item_selected.connect(_on_dr_month_selected)
# Announcement actions
send_mail_btn.pressed.connect(_on_send_mail)
add_reward_btn.pressed.connect(_on_add_reward_pressed)
# =============================================================================
# Core Panel Logic
@@ -623,3 +638,67 @@ func _save_daily_rewards_config() -> void:
_set_status("Save failed: " + res.get("error"), CLR_STATUS_ERR)
else:
_set_status("Config saved successfully!", CLR_STATUS_OK)
# =============================================================================
# TAB 4: ANNOUNCEMENTS
# =============================================================================
func _on_add_reward_pressed() -> void:
var row = reward_row_template.duplicate()
row.visible = true
rewards_list.add_child(row)
var remove_btn = row.get_node("RemoveBtn") as Button
remove_btn.pressed.connect(func(): row.queue_free())
func _on_send_mail() -> void:
var target = target_user_edit.text.strip_edges()
var title = title_edit.text.strip_edges()
var content = content_edit.text.strip_edges()
var start_date = start_date_edit.get_date_iso()
var end_date = end_date_edit.get_date_iso()
if title.is_empty() or content.is_empty():
_set_status("Title and content cannot be empty", CLR_STATUS_ERR)
return
var rewards_arr = []
for child in rewards_list.get_children():
if not child.visible: continue # skip template
var type_opt = child.get_node("TypeOption") as OptionButton
var id_edit = child.get_node("IdEdit") as LineEdit
var amount_spin = child.get_node("AmountSpin") as SpinBox
var type_str = type_opt.get_item_text(type_opt.selected)
var r_id = id_edit.text.strip_edges()
rewards_arr.append({
"type": type_str,
"id": r_id,
"amount": int(amount_spin.value)
})
send_mail_btn.disabled = true
_set_status("Sending mail...")
var payload = {
"target_user_id": target,
"title": title,
"content": content,
"start_date": start_date,
"end_date": end_date,
"rewards": rewards_arr
}
var res = await _rpc("admin_send_mail", payload)
send_mail_btn.disabled = false
if res.has("error"):
_set_status("Failed to send mail: " + res.get("error", "Unknown"), CLR_STATUS_ERR)
else:
_set_status("Mail sent successfully!", CLR_STATUS_OK)
title_edit.text = ""
content_edit.text = ""
if start_date_edit.has_method("clear_date"):
start_date_edit.clear_date()
if end_date_edit.has_method("clear_date"):
end_date_edit.clear_date()
for child in rewards_list.get_children():
if child.visible: child.queue_free()
+13 -9
View File
@@ -6,13 +6,15 @@ extends Control
@onready var progress_bar := %ProgressBar as ProgressBar
@onready var progress_label := %ProgressLabel as Label
@onready var button_container := %ButtonContainer as HBoxContainer
@onready var update_button := %UpdateButton as Button
@onready var skip_button := %SkipButton as Button
@onready var update_button := %UpdateButton as BaseButton
@onready var update_button_label := %UpdateButton.get_node("Label") as Label
@onready var skip_button := %SkipButton as BaseButton
@onready var skip_button_label := %SkipButton.get_node("Label") as Label
@onready var changelog_panel := %ChangelogPanel
@onready var changelog_richtext := %ChangelogRichText as RichTextLabel
@onready var prev_button := %PrevButton as Button
@onready var next_button := %NextButton as Button
@onready var prev_button := %PrevButton as BaseButton
@onready var next_button := %NextButton as BaseButton
@onready var page_label := %PageLabel as Label
var update_manager: Node
@@ -73,20 +75,20 @@ func _on_update_check_completed(has_update: bool, info: Dictionary) -> void:
button_container.visible = true
update_button.visible = true
skip_button.visible = true
skip_button.text = "Play without updating"
skip_button_label.text = "Play without updating"
else:
status_label.text = "Game up to date."
button_container.visible = true
update_button.visible = false
skip_button.visible = true
skip_button.text = "Play Game"
skip_button_label.text = "Play"
func _on_update_check_failed(_error: String) -> void:
status_label.text = "Could not check for updates"
button_container.visible = true
update_button.visible = false
skip_button.visible = true
skip_button.text = "Play Offline"
skip_button_label.text = "Play"
func _on_update_pressed() -> void:
button_container.visible = false
@@ -114,7 +116,7 @@ func _on_download_failed(error: String) -> void:
progress_bar.visible = false
progress_label.visible = false
button_container.visible = true
update_button.text = "Retry"
update_button_label.text = "Retry"
func _begin_resource_load() -> void:
button_container.visible = false
@@ -156,8 +158,10 @@ func _update_pagination_ui():
var txt: String = "[font_size=20][b]Version " + str(entry.get("version", "Unknown")) + "[/b][/font_size]\n[color=gray]" + str(entry.get("date", "")) + "[/color]\n\n"
var changes: Array = entry.get("changelog", [])
txt += "[table=2]"
for change in changes:
txt += "" + change + "\n"
txt += "[cell]• [/cell][cell]" + change + "[/cell]"
txt += "[/table]"
changelog_richtext.text = txt
+97
View File
@@ -0,0 +1,97 @@
extends Button
signal date_selected(date_str: String)
@onready var popup = $PopupPanel
@onready var month_year_lbl = $PopupPanel/VBox/Header/MonthYearLbl
@onready var days_grid = $PopupPanel/VBox/DaysGrid
@onready var prev_btn = $PopupPanel/VBox/Header/PrevBtn
@onready var next_btn = $PopupPanel/VBox/Header/NextBtn
@onready var clear_btn = $PopupPanel/VBox/ClearBtn
var current_date: Dictionary
var view_date: Dictionary
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
func _ready() -> void:
var now = Time.get_datetime_dict_from_system()
view_date = {"year": now.year, "month": now.month, "day": now.day}
pressed.connect(_on_button_pressed)
prev_btn.pressed.connect(_on_prev_month)
next_btn.pressed.connect(_on_next_month)
clear_btn.pressed.connect(_on_clear_pressed)
_update_calendar()
func _on_button_pressed() -> void:
_update_calendar()
popup.popup_centered()
func _on_prev_month() -> void:
view_date.month -= 1
if view_date.month < 1:
view_date.month = 12
view_date.year -= 1
_update_calendar()
func _on_next_month() -> void:
view_date.month += 1
if view_date.month > 12:
view_date.month = 1
view_date.year += 1
_update_calendar()
func _on_clear_pressed() -> void:
clear_date()
func clear_date() -> void:
current_date = {}
text = "Select Date..."
emit_signal("date_selected", "")
popup.hide()
func _update_calendar() -> void:
month_year_lbl.text = str(MONTHS[view_date.month - 1]) + " " + str(view_date.year)
for child in days_grid.get_children():
child.queue_free()
var first_day_dict = {"year": view_date.year, "month": view_date.month, "day": 1}
var first_day_unix = Time.get_unix_time_from_datetime_dict(first_day_dict)
var first_day_weekday = Time.get_datetime_dict_from_unix_time(first_day_unix).weekday
# Godot weekday: 0 = Sunday, 1 = Monday...
for i in range(first_day_weekday):
var empty = Control.new()
days_grid.add_child(empty)
var days_in_month = _get_days_in_month(view_date.month, view_date.year)
for d in range(1, days_in_month + 1):
var btn = Button.new()
btn.text = str(d)
btn.custom_minimum_size = Vector2(32, 32)
if not current_date.is_empty() and current_date.year == view_date.year and current_date.month == view_date.month and current_date.day == d:
btn.add_theme_color_override("font_color", Color.YELLOW)
btn.pressed.connect(_on_day_clicked.bind(d))
days_grid.add_child(btn)
func _on_day_clicked(day: int) -> void:
current_date = {"year": view_date.year, "month": view_date.month, "day": day}
var date_str = "%04d-%02d-%02dT00:00:00Z" % [current_date.year, current_date.month, current_date.day]
text = "%04d-%02d-%02d" % [current_date.year, current_date.month, current_date.day]
emit_signal("date_selected", date_str)
popup.hide()
func get_date_iso() -> String:
if current_date.is_empty():
return ""
return "%04d-%02d-%02dT00:00:00Z" % [current_date.year, current_date.month, current_date.day]
func _get_days_in_month(month: int, year: int) -> int:
if month in [1, 3, 5, 7, 8, 10, 12]: return 31
if month in [4, 6, 9, 11]: return 30
if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0): return 29
return 28
+1
View File
@@ -0,0 +1 @@
uid://0rwrfgvuqgrb
+42 -10
View File
@@ -1,12 +1,12 @@
extends HBoxContainer
extends PanelContainer
## FriendRow — Single row in the friends list inside SocialPanel.
@onready var _name_label: Label = %NameLabel
@onready var _state_label: Label = %StateLabel
@onready var _dm_btn: Button = %DMBtn
@onready var _accept_btn: Button = %AcceptBtn
@onready var _decline_btn: Button = %DeclineBtn
@onready var _remove_btn: Button = %RemoveBtn
@onready var _dm_btn: TextureButton = %DMBtn
@onready var _accept_btn: TextureButton = %AcceptBtn
@onready var _decline_btn: TextureButton = %DeclineBtn
@onready var _remove_btn: TextureButton = %RemoveBtn
var _user_id: String
var _username: String
@@ -19,17 +19,49 @@ func setup(uid: String, uname: String, state: int, panel: Control) -> void:
_name_label.text = uname
_dm_btn.visible = false
_accept_btn.visible = false
_decline_btn.visible = false
_remove_btn.visible = false
_state_label.visible = false
match state:
-1: # Search result (Not friend)
_accept_btn.visible = true
if not _accept_btn.pressed.is_connected(_on_accept):
_accept_btn.pressed.connect(_on_accept)
FriendManager.STATE_FRIEND:
_dm_btn.visible = true
_state_label.visible = true
_dm_btn.visible = true
_remove_btn.visible = true
_dm_btn.pressed.connect(func(): panel.call("open_dm", uid, uname))
_remove_btn.pressed.connect(func(): FriendManager.remove_friend(uid))
if not _dm_btn.pressed.is_connected(_on_dm):
_dm_btn.pressed.connect(_on_dm)
if not _remove_btn.pressed.is_connected(_on_remove):
_remove_btn.pressed.connect(_on_remove)
FriendManager.STATE_INVITE_OUT:
_state_label.text = "(invite sent)"
_state_label.visible = true
FriendManager.STATE_INVITE_IN:
_state_label.text = "Incoming Request"
_state_label.visible = true
_accept_btn.visible = true
_decline_btn.visible = true
_accept_btn.pressed.connect(func(): FriendManager.add_friend_by_id(uid))
_decline_btn.pressed.connect(func(): FriendManager.remove_friend(uid))
if not _accept_btn.pressed.is_connected(_on_accept):
_accept_btn.pressed.connect(_on_accept)
if not _decline_btn.pressed.is_connected(_on_remove):
_decline_btn.pressed.connect(_on_remove)
func _on_dm():
_panel.call("open_dm", _user_id, _username)
func _on_remove():
FriendManager.remove_friend(_user_id)
queue_free()
func _on_accept():
FriendManager.add_friend_by_id(_user_id)
_accept_btn.visible = false
_decline_btn.visible = false
_dm_btn.visible = true
_remove_btn.visible = true
_state_label.text = "LAST ONLINE: Just now"
+18 -1
View File
@@ -27,6 +27,10 @@ extends Control
@onready var register_button := %RegisterButton as Button
@onready var reg_status_label := %RegStatusLabel as Label
# Tab navigation buttons (left column)
@onready var sign_in_tab_btn := %SignInTabBtn as TextureButton
@onready var sign_up_tab_btn := %SignUpTabBtn as TextureButton
var current_captcha_answer: int = 0
var is_loading: bool = false
@@ -87,6 +91,10 @@ func _connect_signals() -> void:
lan_host_btn.pressed.connect(_on_lan_host_pressed)
lan_join_btn.pressed.connect(func(): _on_lan_join_pressed(lan_ip.text))
# Tab navigation buttons
sign_in_tab_btn.pressed.connect(func(): _switch_tab(0))
sign_up_tab_btn.pressed.connect(func(): _switch_tab(1))
# Enter key to submit
password_input.text_submitted.connect(func(_t): _on_login_pressed())
reg_confirm_password_input.text_submitted.connect(func(_t): _on_register_pressed())
@@ -95,7 +103,7 @@ func _setup_ui() -> void:
status_label.text = ""
reg_status_label.text = ""
loading_spinner.visible = false
tab_container.current_tab = 0
_switch_tab(0)
# Hide social buttons on platforms where they're not supported
_configure_social_buttons()
@@ -118,6 +126,15 @@ func _configure_social_buttons() -> void:
# Panel Switching
# =============================================================================
func _switch_tab(tab: int) -> void:
tab_container.current_tab = tab
_update_tab_visuals(tab)
_on_tab_changed(tab)
func _update_tab_visuals(tab: int) -> void:
sign_in_tab_btn.button_pressed = (tab == 0)
sign_up_tab_btn.button_pressed = (tab == 1)
func _on_tab_changed(tab: int) -> void:
if tab == 0:
status_label.text = ""
+180
View File
@@ -0,0 +1,180 @@
extends Panel
signal closed
@onready var close_btn := %CloseBtn as Button
@onready var mail_list_vbox := %MailListVBox as VBoxContainer
@onready var mail_title_lbl := %MailTitleLbl as Label
@onready var mail_content_text := %MailContentText as RichTextLabel
@onready var sender_lbl := %SenderLbl as Label
@onready var dynamic_rewards_container := %DynamicRewardsContainer as HBoxContainer
@onready var reward_hbox_template := %RewardHBoxTemplate as HBoxContainer
@onready var action_btn := %ActionBtn as Button
@onready var empty_state_lbl := %EmptyStateLbl as Label
var _current_mail: Dictionary = {}
func _ready() -> void:
visible = false
close_btn.pressed.connect(hide_panel)
action_btn.pressed.connect(_on_action_pressed)
if MailManager:
MailManager.mail_updated.connect(_refresh_ui)
func show_panel() -> void:
visible = true
_clear_details()
if MailManager:
MailManager.fetch_mails()
_refresh_ui()
func hide_panel() -> void:
visible = false
emit_signal("closed")
func _refresh_ui() -> void:
if not visible or not MailManager: return
for child in mail_list_vbox.get_children():
child.queue_free()
var mails = MailManager.mails
if mails.is_empty():
empty_state_lbl.visible = true
else:
empty_state_lbl.visible = false
for i in range(mails.size()):
var mail = mails[i]
var btn = _create_mail_button(mail)
mail_list_vbox.add_child(btn)
btn.pressed.connect(_on_mail_selected.bind(mail))
if not _current_mail.is_empty() and _current_mail.get("id") == mail.get("id"):
_current_mail = mail
_update_details(mail)
btn.button_pressed = true
if _current_mail.is_empty() and mails.size() > 0:
_on_mail_selected(mails[0])
@onready var mail_btn_template := %MailBtnTemplate as Button
func _create_mail_button(mail: Dictionary) -> Button:
var btn = mail_btn_template.duplicate()
btn.visible = true
var title = btn.get_node("VBox/Title") as Label
title.text = mail.get("title", "No Title")
var date_lbl = btn.get_node("VBox/HBox/DateLbl") as Label
var date_str = mail.get("date", "")
var expiry_str = mail.get("expiry_date", "")
var label_text = date_str.substr(0, 10) if date_str.length() >= 10 else date_str
if not expiry_str.is_empty():
var now = Time.get_unix_time_from_system()
var expiry_unix = Time.get_unix_time_from_datetime_string(expiry_str)
var diff = expiry_unix - now
if diff > 0:
var days = int(diff / 86400)
if days > 0:
label_text += " (Exp: %dd)" % days
else:
label_text += " (Exp: <1d)"
else:
label_text += " (Expired)"
date_lbl.text = label_text
var status_lbl = btn.get_node("VBox/HBox/StatusLbl") as Label
var mail_id = mail.get("id", "")
if mail_id in MailManager.claimed_ids:
status_lbl.text = "CLAIMED"
status_lbl.add_theme_color_override("font_color", Color.GREEN)
elif mail_id in MailManager.read_ids:
status_lbl.text = "READ"
status_lbl.add_theme_color_override("font_color", Color.GRAY)
else:
status_lbl.text = "NEW"
status_lbl.add_theme_color_override("font_color", Color.YELLOW)
return btn
func _on_mail_selected(mail: Dictionary) -> void:
for child in mail_list_vbox.get_children():
if child is Button:
child.button_pressed = false
_current_mail = mail
_update_details(mail)
MailManager.mark_as_read(mail.get("id", ""))
func _clear_details() -> void:
mail_title_lbl.text = ""
mail_content_text.text = ""
sender_lbl.text = ""
for child in dynamic_rewards_container.get_children():
if child.visible: child.queue_free()
action_btn.hide()
func _update_details(mail: Dictionary) -> void:
mail_title_lbl.text = mail.get("title", "No Title")
mail_content_text.text = mail.get("content", "")
sender_lbl.text = "SENDER:\n" + mail.get("sender", "SYSTEM")
for child in dynamic_rewards_container.get_children():
if child.visible: child.queue_free()
var rewards = mail.get("rewards", [])
var has_rewards = false
# Legacy dictionary support
if typeof(rewards) == TYPE_DICTIONARY:
var arr = []
if rewards.get("star", 0) > 0: arr.append({"type": "star", "amount": rewards.star})
if rewards.get("gold", 0) > 0: arr.append({"type": "gold", "amount": rewards.gold})
rewards = arr
for r in rewards:
has_rewards = true
var row = reward_hbox_template.duplicate()
row.visible = true
dynamic_rewards_container.add_child(row)
var amt_lbl = row.get_node("AmountLbl") as Label
var t = r.get("type", "star")
var amt = r.get("amount", 0)
var id = r.get("id", "")
if t == "star" or t == "gold":
amt_lbl.text = str(amt) + " " + t.to_upper()
else:
amt_lbl.text = str(amt) + " " + id
action_btn.show()
var mail_id = mail.get("id", "")
if has_rewards and mail_id not in MailManager.claimed_ids:
action_btn.text = "CLAIM"
action_btn.add_theme_color_override("font_color", Color.WHITE)
else:
action_btn.text = "DELETE"
action_btn.add_theme_color_override("font_color", Color.RED)
func _on_action_pressed() -> void:
if _current_mail.is_empty(): return
var mail_id = _current_mail.get("id", "")
if action_btn.text == "CLAIM":
action_btn.disabled = true
var ok = await MailManager.claim_reward(mail_id)
action_btn.disabled = false
if ok:
_update_details(_current_mail)
else:
action_btn.disabled = true
var ok = await MailManager.delete_mail(mail_id)
action_btn.disabled = false
if ok:
_current_mail = {}
_clear_details()
+1
View File
@@ -0,0 +1 @@
uid://b5fema68m6b2s
+32 -79
View File
@@ -9,31 +9,28 @@ signal closed
# Tab buttons
@onready var _search_tab_btn: Button = %SearchTabBtn
@onready var _requests_tab_btn: Button = %RequestsTabBtn
@onready var _friends_tab_btn: Button = %FriendsTabBtn
@onready var _dm_tab_btn: Button = %DMTabBtn
@onready var _dm_tab_btn: Button = get_node_or_null("%DMTabBtn")
# Views
@onready var _search_view: VBoxContainer = %SearchView
@onready var _requests_view: VBoxContainer = %RequestsView
@onready var _friends_view: VBoxContainer = %FriendsView
@onready var _dm_view: VBoxContainer = %DMView
@onready var _dm_view: PanelContainer = %DMView
# Search tab nodes
@onready var _search_input: LineEdit = %SearchInput
@onready var _search_btn: Button = %SearchBtn
@onready var _no_search_results: Label = %NoSearchResultsLabel
@onready var _search_results_list: VBoxContainer = %SearchResultsList
@onready var _search_result_tmpl: HBoxContainer = %SearchResultTemplate
@onready var _search_results_list: GridContainer = %SearchResultsList
# Requests tab nodes
@onready var _no_requests_label: Label = %NoRequestsLabel
@onready var _requests_list: VBoxContainer = %RequestsList
@onready var _request_row_tmpl: HBoxContainer = %RequestRowTemplate
# Friends tab nodes
@onready var _no_friends_label: Label = %NoFriendsLabel
@onready var _friend_list: VBoxContainer = %FriendList
@onready var _friend_list: GridContainer = %FriendList
# DM tab nodes
@onready var _dm_back_btn: Button = %DMBackBtn
@@ -53,10 +50,12 @@ func _ready() -> void:
# Tab buttons
_search_tab_btn.pressed.connect(func(): _show_tab("search"))
_requests_tab_btn.pressed.connect(func(): _show_tab("requests"))
_friends_tab_btn.pressed.connect(func(): _show_tab("friends"))
if _dm_tab_btn:
_dm_tab_btn.pressed.connect(func(): _show_tab("dm"))
_dm_back_btn.pressed.connect(func(): _show_tab("friends"))
# Search
_search_btn.pressed.connect(_on_search_pressed)
_search_input.text_submitted.connect(func(_t): _on_search_pressed())
@@ -81,7 +80,6 @@ func _ready() -> void:
func _show_tab(tab: String) -> void:
_current_tab = tab
_search_view.visible = tab == "search"
_requests_view.visible = tab == "requests"
_friends_view.visible = tab == "friends"
_dm_view.visible = tab == "dm"
@@ -121,60 +119,26 @@ func _populate_search_results(users: Array) -> void:
_no_search_results.visible = users.is_empty()
var my_id = NakamaManager.session.user_id if NakamaManager.session else ""
var friend_row_scene := preload("res://scenes/ui/friend_row.tscn")
for u in users:
if u.user_id == my_id:
continue # skip self
var row: HBoxContainer = _search_result_tmpl.duplicate()
row.show()
row.get_node("SRNameLabel").text = u.display_name + " (@" + u.username + ")"
var add_btn: Button = row.get_node("SRAddBtn")
add_btn.pressed.connect(func():
FriendManager.add_friend_by_id(u.user_id)
add_btn.text = "Sent ✓"
add_btn.disabled = true
)
var row: Control = friend_row_scene.instantiate()
_search_results_list.add_child(row)
row.setup(u.user_id, u.username, -1, self)
# ─── Requests Tab ────────────────────────────────────────────────────────────
func _populate_requests(incoming: Array) -> void:
for ch in _requests_list.get_children():
ch.queue_free()
_no_requests_label.visible = incoming.is_empty()
for f in incoming:
var row: HBoxContainer = _request_row_tmpl.duplicate()
row.show()
row.get_node("RRNameLabel").text = f.username
var accept_btn: Button = row.get_node("RRAcceptBtn")
var decline_btn: Button = row.get_node("RRDeclineBtn")
var uid: String = f.user_id
accept_btn.pressed.connect(func():
FriendManager.add_friend_by_id(uid)
row.queue_free()
)
decline_btn.pressed.connect(func():
FriendManager.remove_friend(uid)
row.queue_free()
)
_requests_list.add_child(row)
# Badge on tab button
if incoming.is_empty():
_requests_tab_btn.text = "Requests"
else:
_requests_tab_btn.text = "Requests (%d)" % incoming.size()
# ─── Friends Tab ─────────────────────────────────────────────────────────────
func _populate_friends(mutual: Array) -> void:
func _populate_friends(friends_array: Array) -> void:
for ch in _friend_list.get_children():
ch.queue_free()
_no_friends_label.visible = mutual.is_empty()
_no_friends_label.visible = friends_array.is_empty()
var friend_row_scene := preload("res://scenes/ui/friend_row.tscn")
for f in mutual:
for f in friends_array:
var row: Control = friend_row_scene.instantiate()
_friend_list.add_child(row)
row.setup(f.user_id, f.username, f.state, self)
@@ -182,35 +146,24 @@ func _populate_friends(mutual: Array) -> void:
# ─── FriendManager Callbacks ─────────────────────────────────────────────────
func _on_friends_updated(friends: Array) -> void:
print("[SocialPanel] _on_friends_updated: total=%d" % friends.size())
var incoming := friends.filter(func(f): return f.state == FriendManager.STATE_INVITE_IN)
var mutual := friends.filter(func(f): return f.state == FriendManager.STATE_FRIEND)
print("[SocialPanel] incoming=%d mutual=%d" % [incoming.size(), mutual.size()])
_populate_requests(incoming)
_populate_friends(mutual)
# Pass both incoming and mutual friends to the friends list
var display_list := friends.filter(func(f): return f.state == FriendManager.STATE_INVITE_IN or f.state == FriendManager.STATE_FRIEND)
# Also update the badge on the Friends tab if there are incoming requests
var incoming_count = friends.filter(func(f): return f.state == FriendManager.STATE_INVITE_IN).size()
if incoming_count > 0:
_friends_tab_btn.text = "FRIENDS (%d)" % incoming_count
else:
_friends_tab_btn.text = "FRIENDS"
_populate_friends(display_list)
signal dm_requested(user_id: String, username: String)
# ─── DM ──────────────────────────────────────────────────────────────────────
func open_dm(user_id: String, username: String) -> void:
_active_dm_user_id = user_id
_dm_username_label.text = "DM: %s" % username
_dm_tab_btn.visible = true
_dm_log.clear()
_show_tab("dm")
_dm_log.append_text("[i]Loading history...[/i]\n")
var history = await FriendManager.get_dm_history(user_id)
_dm_log.clear()
_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
_dm_history[user_id].append({"from": "me" if is_self else entry.get("from"), "msg": entry.get("msg")})
_dm_log.append_text("[b]%s:[/b] %s\n" % [sender_name, entry.get("msg", "")])
FriendManager.open_dm(user_id)
emit_signal("dm_requested", user_id, username)
func _send_dm() -> void:
var text = _dm_input.text.strip_edges()