feat: update chat
This commit is contained in:
+200
-22
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user