1311 lines
47 KiB
GDScript
1311 lines
47 KiB
GDScript
extends Control
|
|
|
|
# UI References - Main Menu
|
|
@onready var main_menu_panel = $MainMenuPanel
|
|
@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 = %ServerOption
|
|
@onready var server_ip_input = %ServerIPInput
|
|
|
|
# Leaderboard Reference
|
|
@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
|
|
@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
|
|
|
|
# UI References - Lobby Panel
|
|
@onready var lobby_panel = $LobbyPanel
|
|
@onready var room_name_header = $LobbyPanel/RoomNameHeader
|
|
@onready var host_banner = $LobbyPanel/HostBanner
|
|
@onready var host_banner_label = $LobbyPanel/HostBanner/HostBannerLabel
|
|
|
|
|
|
# UI References - Top Bar
|
|
@onready var profile_btn = $LobbyPanel/TopBar/ProfileSection/ProfileBtn
|
|
@onready var logout_btn = $LobbyPanel/TopBar/ProfileSection/LogoutBtn
|
|
@onready var match_id_display = $LobbyPanel/TopBar/MatchIdContainer/MatchIdDisplay
|
|
@onready var copy_id_btn = $LobbyPanel/TopBar/MatchIdContainer/CopyIdBtn
|
|
@onready var duration_option = $LobbyPanel/TopBar/SettingsSection/DurationOption
|
|
@onready var duration_text_label = $LobbyPanel/TopBar/SettingsSection/DurationTextLabel
|
|
@onready var lobby_top_settings_btn = $LobbyPanel/TopBar/ProfileSection/LobbySettingsBtn
|
|
@onready var random_spawn_check = $LobbyPanel/TopBar/SettingsSection/RandomSpawnCheck
|
|
@onready var random_spawn_label = $LobbyPanel/TopBar/SettingsSection/RandomSpawnLabel
|
|
@onready var enable_timer_check = $LobbyPanel/TopBar/SettingsSection/EnableTimerCheck
|
|
@onready var enable_timer_label = $LobbyPanel/TopBar/SettingsSection/EnableTimerLabel
|
|
@onready var scarcity_option = $LobbyPanel/TopBar/SettingsSection/ScarcityOption
|
|
@onready var scarcity_label = $LobbyPanel/TopBar/SettingsSection/ScarcityLabel
|
|
@onready var scarcity_spacer = $LobbyPanel/TopBar/SettingsSection/ScarcitySpacer
|
|
@onready var spawn_spacer = $LobbyPanel/TopBar/SettingsSection/SpawnSpacer
|
|
@onready var timer_spacer = $LobbyPanel/TopBar/SettingsSection/TimerSpacer
|
|
@onready var game_mode_option = $LobbyPanel/TopBar/SettingsSection/GameModeOption
|
|
@onready var game_mode_text_label = $LobbyPanel/TopBar/SettingsSection/GameModeTextLabel
|
|
|
|
# Custom Settings Containers
|
|
var sng_settings_container: HBoxContainer
|
|
var sng_go_option: OptionButton
|
|
var sng_stop_option: OptionButton
|
|
var sng_goals_option: OptionButton
|
|
|
|
var doors_settings_container: HBoxContainer
|
|
var doors_swap_option: OptionButton
|
|
var doors_refresh_option: OptionButton
|
|
var doors_goals_option: OptionButton
|
|
|
|
# UI References - Player Slots
|
|
@onready var players_container = $LobbyPanel/PlayersContainer
|
|
@onready var players_container2 = $LobbyPanel/PlayersContainer2
|
|
@onready var player_slots: Array[Control] = []
|
|
|
|
# UI References - Area Selector
|
|
@onready var area_selector = $LobbyPanel/AreaSelector
|
|
@onready var area_left_btn = $LobbyPanel/AreaSelector/AreaLeftBtn
|
|
@onready var area_name_label = $LobbyPanel/AreaSelector/AreaName
|
|
@onready var area_right_btn = $LobbyPanel/AreaSelector/AreaRightBtn
|
|
|
|
# UI References - Bottom Bar
|
|
@onready var leave_btn = $LobbyPanel/BottomBar/LeaveBtn
|
|
@onready var ready_btn = $LobbyPanel/BottomBar/ReadyBtn
|
|
@onready var start_game_btn = $LobbyPanel/BottomBar/StartGameBtn
|
|
@onready var status_label = $LobbyPanel/StatusLabel
|
|
|
|
# UI References - Status
|
|
@onready var connection_status = $StatusBar/ConnectionStatus
|
|
|
|
# Character preview textures
|
|
var character_textures: Dictionary = {}
|
|
|
|
# Profile panel instance
|
|
var profile_panel_instance: Control
|
|
var admin_panel_instance: Control
|
|
|
|
# Store current match ID for copy function
|
|
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 = {}
|
|
|
|
# =============================================================================
|
|
# Chat System
|
|
# =============================================================================
|
|
const GLOBAL_CHAT_ROOM := "global_lobby"
|
|
var _chat_channel = null
|
|
var _chat_messages: Array = []
|
|
|
|
@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
|
|
# var server_ip_input: LineEdit
|
|
|
|
|
|
func _ready():
|
|
# Start background music for Lobby
|
|
MusicManager.start_music()
|
|
|
|
# Load character textures
|
|
_load_character_textures()
|
|
|
|
# Server config UI is now in tscn
|
|
|
|
# Get player slot references
|
|
_setup_player_slots()
|
|
|
|
# Setup Game Mode specific UI dynamically
|
|
_create_custom_settings_ui()
|
|
|
|
# Initial UI update
|
|
_on_profile_updated()
|
|
|
|
# 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 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)
|
|
|
|
# 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)
|
|
if quit_btn:
|
|
quit_btn.pressed.connect(_on_quit_pressed)
|
|
|
|
# Connect Server Selection signals
|
|
if server_option:
|
|
server_option.item_selected.connect(_on_server_option_selected)
|
|
# Initialize state
|
|
_on_server_option_selected(server_option.selected)
|
|
if server_ip_input:
|
|
server_ip_input.text_submitted.connect(_on_server_ip_submitted)
|
|
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 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 button signals - Lobby
|
|
profile_btn.pressed.connect(_on_profile_btn_pressed)
|
|
if logout_btn:
|
|
logout_btn.pressed.connect(_on_logout_pressed)
|
|
if lobby_top_settings_btn:
|
|
lobby_top_settings_btn.pressed.connect(_on_settings_pressed)
|
|
copy_id_btn.pressed.connect(_on_copy_id_pressed)
|
|
duration_option.item_selected.connect(_on_duration_selected)
|
|
random_spawn_check.toggled.connect(_on_random_spawn_toggled)
|
|
enable_timer_check.toggled.connect(_on_enable_timer_toggled)
|
|
area_left_btn.pressed.connect(func(): LobbyManager.cycle_area(-1))
|
|
area_right_btn.pressed.connect(func(): LobbyManager.cycle_area(1))
|
|
leave_btn.pressed.connect(_on_leave_pressed)
|
|
ready_btn.toggled.connect(_on_ready_toggled)
|
|
start_game_btn.pressed.connect(_on_start_game_pressed)
|
|
|
|
if scarcity_option:
|
|
scarcity_option.item_selected.connect(_on_scarcity_selected)
|
|
|
|
if game_mode_option:
|
|
game_mode_option.item_selected.connect(_on_game_mode_selected)
|
|
_setup_game_modes()
|
|
|
|
# Connect LobbyManager signals
|
|
LobbyManager.room_list_updated.connect(_on_room_list_updated)
|
|
LobbyManager.room_joined.connect(_on_room_joined)
|
|
LobbyManager.room_left.connect(_on_room_left)
|
|
LobbyManager.host_disconnected.connect(_on_host_disconnected)
|
|
LobbyManager.player_joined.connect(_on_player_joined)
|
|
LobbyManager.player_left.connect(_on_player_left)
|
|
LobbyManager.ready_state_changed.connect(_on_ready_state_changed)
|
|
LobbyManager.all_players_ready.connect(_on_all_players_ready)
|
|
LobbyManager.game_starting.connect(_on_game_starting)
|
|
LobbyManager.match_duration_changed.connect(_on_match_duration_changed)
|
|
LobbyManager.randomize_spawn_changed.connect(_on_randomize_spawn_changed)
|
|
LobbyManager.enable_cycle_timer_changed.connect(_on_enable_cycle_timer_changed)
|
|
LobbyManager.character_changed.connect(_on_character_changed)
|
|
LobbyManager.area_changed.connect(_on_area_changed)
|
|
LobbyManager.scarcity_mode_changed.connect(_on_scarcity_mode_changed)
|
|
LobbyManager.game_mode_changed.connect(_on_game_mode_changed)
|
|
LobbyManager.player_list_changed.connect(_update_player_slots)
|
|
|
|
LobbyManager.sng_go_duration_changed.connect(_on_sng_update)
|
|
LobbyManager.sng_stop_duration_changed.connect(_on_sng_update)
|
|
LobbyManager.sng_required_goals_changed.connect(_on_sng_update)
|
|
LobbyManager.doors_swap_time_changed.connect(_on_doors_update)
|
|
LobbyManager.doors_refresh_time_changed.connect(_on_doors_update)
|
|
LobbyManager.doors_required_goals_changed.connect(_on_doors_update)
|
|
|
|
# Connect NakamaManager signals
|
|
NakamaManager.connected_to_nakama.connect(_on_connected_to_nakama)
|
|
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)
|
|
|
|
# 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()
|
|
|
|
# Show main menu initially
|
|
_show_panel("main_menu")
|
|
|
|
# Check for disconnection reason from manager
|
|
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
|
|
# =============================================================================
|
|
|
|
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])
|
|
|
|
# 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."""
|
|
var characters = {
|
|
"Copper": "res://assets/graphics/character_selection/sc_characters/sc_copper.png",
|
|
"Dabro": "res://assets/graphics/character_selection/sc_characters/sc_dabro.png",
|
|
"Gatot": "res://assets/graphics/character_selection/sc_characters/sc_gatot.png",
|
|
"Pip": "res://assets/graphics/character_selection/sc_characters/sc_pip.png",
|
|
"Random": "res://assets/graphics/character_selection/sc_characters/sc_unknown.png"
|
|
}
|
|
for char_name in characters:
|
|
var tex_path = characters[char_name]
|
|
if ResourceLoader.exists(tex_path):
|
|
character_textures[char_name] = load(tex_path)
|
|
else:
|
|
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
|
|
NakamaManager.set_server("localhost")
|
|
LobbyManager.is_lan_mode = false
|
|
connection_status.text = "Mode: Local Testing (Nakama)"
|
|
elif index == 1:
|
|
# Nakama Remote
|
|
if server_ip_input:
|
|
server_ip_input.visible = true
|
|
server_ip_input.placeholder_text = "IP (100.x) or Tailscale Funnel URL..."
|
|
if server_ip_input: NakamaManager.set_server(server_ip_input.text)
|
|
LobbyManager.is_lan_mode = false
|
|
connection_status.text = "Mode: Online (Nakama Remote)"
|
|
elif index == 2:
|
|
# LAN Direct
|
|
if server_ip_input: server_ip_input.visible = false
|
|
LobbyManager.is_lan_mode = true
|
|
connection_status.text = "Mode: LAN Direct (No Server)"
|
|
elif index == 3:
|
|
# Tekton Dash EU
|
|
if server_ip_input: server_ip_input.visible = false
|
|
NakamaManager.set_server("tektondash.vps.webdock.cloud")
|
|
LobbyManager.is_lan_mode = false
|
|
connection_status.text = "Mode: Online (Tekton Dash EU)"
|
|
|
|
func _on_server_ip_submitted(new_text: String) -> void:
|
|
if server_option and server_option.selected == 1:
|
|
var host = new_text.strip_edges()
|
|
NakamaManager.set_server(host)
|
|
if host.ends_with(".ts.net"):
|
|
connection_status.text = "Using Tailscale Funnel: " + host
|
|
else:
|
|
connection_status.text = "Server IP updated: " + host
|
|
|
|
func _setup_game_modes() -> void:
|
|
if not game_mode_option: return
|
|
game_mode_option.clear()
|
|
for mode in LobbyManager.available_game_modes:
|
|
game_mode_option.add_item(mode)
|
|
|
|
# Select current mode
|
|
for i in range(game_mode_option.item_count):
|
|
if game_mode_option.get_item_text(i) == LobbyManager.game_mode:
|
|
game_mode_option.selected = i
|
|
break
|
|
|
|
func _setup_player_slots() -> void:
|
|
"""Get references to all player slot nodes."""
|
|
player_slots.clear()
|
|
|
|
# Slots 1-4 in Container 1
|
|
for i in range(1, 5):
|
|
var slot = players_container.get_node_or_null("PlayerSlot%d" % i)
|
|
if slot:
|
|
player_slots.append(slot)
|
|
_connect_slot_signals(slot, i)
|
|
|
|
# Slots 5-8 in Container 2
|
|
if players_container2:
|
|
for i in range(5, 9):
|
|
var slot = players_container2.get_node_or_null("PlayerSlot%d" % i)
|
|
if slot:
|
|
player_slots.append(slot)
|
|
_connect_slot_signals(slot, i)
|
|
|
|
func _connect_slot_signals(slot: Control, i: int):
|
|
# Connect character navigation buttons for all slots
|
|
var left_btn = slot.get_node_or_null("CharacterNav%d/CharLeftBtn%d" % [i, i])
|
|
var right_btn = slot.get_node_or_null("CharacterNav%d/CharRightBtn%d" % [i, i])
|
|
if left_btn:
|
|
left_btn.pressed.connect(func(): LobbyManager.cycle_character(-1))
|
|
if right_btn:
|
|
right_btn.pressed.connect(func(): LobbyManager.cycle_character(1))
|
|
|
|
# =============================================================================
|
|
# Panel Management
|
|
# =============================================================================
|
|
|
|
func _show_panel(panel_name: String) -> void:
|
|
main_menu_panel.visible = panel_name == "main_menu"
|
|
room_list_panel.visible = panel_name == "room_list"
|
|
lobby_panel.visible = panel_name == "lobby"
|
|
|
|
# =============================================================================
|
|
# 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:
|
|
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()
|
|
|
|
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:
|
|
_show_panel("room_list")
|
|
|
|
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)"
|
|
_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)"
|
|
LobbyManager.refresh_room_list()
|
|
|
|
# =============================================================================
|
|
# Room List Button Handlers
|
|
# =============================================================================
|
|
|
|
func _on_refresh_pressed() -> void:
|
|
connection_status.text = "Refreshing..."
|
|
room_list.clear()
|
|
LobbyManager.refresh_room_list()
|
|
|
|
func _on_room_selected(index: int) -> void:
|
|
"""Automatically fill the match_id input when a room is clicked so it's not lost on focus change."""
|
|
if index < LobbyManager.available_rooms.size():
|
|
var room = LobbyManager.available_rooms[index]
|
|
if LobbyManager.is_lan_mode:
|
|
var ip = room.get("ip", "")
|
|
if not ip.is_empty():
|
|
match_id_input.text = ip
|
|
else:
|
|
var mid = room.get("match_id", "")
|
|
if not mid.is_empty():
|
|
match_id_input.text = mid
|
|
|
|
func _on_room_activated(index: int) -> void:
|
|
"""Handle double-clicking a room to join immediately."""
|
|
_on_room_selected(index)
|
|
_on_join_pressed()
|
|
|
|
func _on_join_pressed() -> void:
|
|
var match_id = match_id_input.text.strip_edges()
|
|
|
|
if match_id.is_empty() and not LobbyManager.is_lan_mode:
|
|
var selected_items = room_list.get_selected_items()
|
|
if selected_items.size() == 0:
|
|
connection_status.text = "Please select a room or enter Match ID"
|
|
return
|
|
|
|
var selected_idx = selected_items[0]
|
|
if selected_idx < LobbyManager.available_rooms.size():
|
|
match_id = LobbyManager.available_rooms[selected_idx].get("match_id", "")
|
|
|
|
# Determine player name
|
|
if AuthManager.is_guest:
|
|
if LobbyManager.local_player_name.is_empty():
|
|
LobbyManager.local_player_name = NameGenerator.generate_guest_name()
|
|
else:
|
|
LobbyManager.local_player_name = UserProfileManager.get_display_name()
|
|
|
|
if LobbyManager.is_lan_mode:
|
|
if match_id.is_empty():
|
|
# If nothing entered but something selected in list (discovered), use it
|
|
var selected_items = room_list.get_selected_items()
|
|
if selected_items.size() > 0:
|
|
var idx = selected_items[0]
|
|
if idx < LobbyManager.available_rooms.size():
|
|
match_id = LobbyManager.available_rooms[idx].get("ip", "")
|
|
|
|
if match_id.is_empty():
|
|
connection_status.text = "Enter Host IP to join"
|
|
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
|
|
else:
|
|
if match_id.is_empty():
|
|
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:
|
|
_show_panel("main_menu")
|
|
connection_status.text = ""
|
|
|
|
func _update_settings_visibility() -> void:
|
|
var is_host = LobbyManager.is_host
|
|
var is_freemode = LobbyManager.game_mode == "Freemode"
|
|
|
|
# Duration
|
|
var show_duration = is_freemode
|
|
duration_option.visible = is_host and show_duration
|
|
duration_text_label.visible = not is_host and show_duration
|
|
$LobbyPanel/TopBar/SettingsSection/DurationLabel.visible = show_duration
|
|
|
|
# Random Spawn
|
|
var show_spawn = is_freemode
|
|
random_spawn_check.visible = is_host and show_spawn
|
|
random_spawn_label.visible = not is_host and show_spawn
|
|
if spawn_spacer: spawn_spacer.visible = show_spawn
|
|
|
|
# Timer
|
|
var show_timer = is_freemode
|
|
enable_timer_check.visible = is_host and show_timer
|
|
enable_timer_label.visible = not is_host and show_timer
|
|
if timer_spacer: timer_spacer.visible = show_timer
|
|
|
|
# Scarcity
|
|
var show_scarcity = is_freemode
|
|
if scarcity_option: scarcity_option.visible = is_host and show_scarcity
|
|
if scarcity_label: scarcity_label.visible = not is_host and show_scarcity
|
|
if scarcity_spacer: scarcity_spacer.visible = show_scarcity
|
|
|
|
# Custom mode sets
|
|
var is_sng = LobbyManager.game_mode == "Stop n Go"
|
|
if sng_settings_container:
|
|
sng_settings_container.visible = is_sng
|
|
sng_go_option.disabled = not is_host
|
|
sng_stop_option.disabled = not is_host
|
|
sng_goals_option.disabled = not is_host
|
|
|
|
sng_goals_option.disabled = not is_host
|
|
|
|
func _create_custom_settings_ui() -> void:
|
|
var settings_section = $LobbyPanel/TopBar/SettingsSection
|
|
if not settings_section: return
|
|
|
|
# Stop n Go
|
|
sng_settings_container = HBoxContainer.new()
|
|
sng_settings_container.visible = false
|
|
settings_section.add_child(sng_settings_container)
|
|
|
|
_add_label(sng_settings_container, "Go Time:")
|
|
sng_go_option = OptionButton.new()
|
|
sng_go_option.add_item("10s"); sng_go_option.add_item("15s"); sng_go_option.add_item("25s")
|
|
sng_go_option.item_selected.connect(func(idx): if LobbyManager.is_host: LobbyManager.set_sng_go_duration([10, 15, 25][idx]))
|
|
sng_settings_container.add_child(sng_go_option)
|
|
|
|
_add_label(sng_settings_container, "Stop Time:")
|
|
sng_stop_option = OptionButton.new()
|
|
sng_stop_option.add_item("3s"); sng_stop_option.add_item("4s"); sng_stop_option.add_item("5s")
|
|
sng_stop_option.item_selected.connect(func(idx): if LobbyManager.is_host: LobbyManager.set_sng_stop_duration([3, 4, 5][idx]))
|
|
sng_settings_container.add_child(sng_stop_option)
|
|
|
|
_add_label(sng_settings_container, "Req Goals:")
|
|
sng_goals_option = OptionButton.new()
|
|
sng_goals_option.add_item("5"); sng_goals_option.add_item("8"); sng_goals_option.add_item("12")
|
|
sng_goals_option.item_selected.connect(func(idx): if LobbyManager.is_host: LobbyManager.set_sng_required_goals([5, 8, 12][idx]))
|
|
sng_settings_container.add_child(sng_goals_option)
|
|
|
|
# Move Game Mode selector to the far right
|
|
var gm_spacer = settings_section.get_node_or_null("GameModeSpacer")
|
|
var gm_option = settings_section.get_node_or_null("GameModeOption")
|
|
var gm_label = settings_section.get_node_or_null("GameModeTextLabel")
|
|
|
|
if gm_spacer: settings_section.move_child(gm_spacer, -1)
|
|
if gm_option: settings_section.move_child(gm_option, -1)
|
|
if gm_label: settings_section.move_child(gm_label, -1)
|
|
|
|
func _add_label(parent: Control, text: String):
|
|
var spacer = Control.new()
|
|
spacer.custom_minimum_size = Vector2(10, 0)
|
|
parent.add_child(spacer)
|
|
var lbl = Label.new()
|
|
lbl.text = text
|
|
parent.add_child(lbl)
|
|
|
|
# =============================================================================
|
|
# Lobby Button Handlers
|
|
# =============================================================================
|
|
|
|
func _on_ready_toggled(is_ready: bool) -> void:
|
|
LobbyManager.set_ready(is_ready)
|
|
ready_btn.text = "READY ✓" if is_ready else "READY"
|
|
|
|
func _on_start_game_pressed() -> void:
|
|
LobbyManager.start_game()
|
|
|
|
func _on_leave_pressed() -> void:
|
|
LobbyManager.leave_room()
|
|
_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)
|
|
status_label.text = "Match ID copied!"
|
|
|
|
func _on_duration_selected(index: int) -> void:
|
|
if not LobbyManager.is_host:
|
|
return
|
|
|
|
var durations = [60, 120, 180, 300, 600]
|
|
if index >= 0 and index < durations.size():
|
|
LobbyManager.set_match_duration(durations[index])
|
|
|
|
func _on_random_spawn_toggled(toggled_on):
|
|
LobbyManager.set_randomize_spawn(toggled_on)
|
|
|
|
func _on_enable_timer_toggled(toggled_on):
|
|
LobbyManager.set_enable_cycle_timer(toggled_on)
|
|
|
|
func _on_scarcity_selected(index: int) -> void:
|
|
if not LobbyManager.is_host: return
|
|
var mode = scarcity_option.get_item_text(index)
|
|
LobbyManager.set_scarcity_mode(mode)
|
|
|
|
func _on_scarcity_mode_changed(mode: String) -> void:
|
|
if scarcity_option:
|
|
for i in range(scarcity_option.item_count):
|
|
if scarcity_option.get_item_text(i) == mode:
|
|
scarcity_option.selected = i
|
|
break
|
|
if scarcity_label:
|
|
scarcity_label.text = mode
|
|
|
|
func _on_game_mode_selected(index: int) -> void:
|
|
if not LobbyManager.is_host: return
|
|
var mode = game_mode_option.get_item_text(index)
|
|
LobbyManager.set_game_mode(mode)
|
|
|
|
func _on_game_mode_changed(mode: String) -> void:
|
|
if game_mode_option:
|
|
for i in range(game_mode_option.item_count):
|
|
if game_mode_option.get_item_text(i) == mode:
|
|
game_mode_option.selected = i
|
|
break
|
|
if game_mode_text_label:
|
|
game_mode_text_label.text = mode
|
|
|
|
_update_settings_visibility()
|
|
|
|
func _on_sng_update(_val: int = 0) -> void:
|
|
if not sng_go_option: return
|
|
|
|
var go_idx = [10, 15, 25].find(LobbyManager.sng_go_duration)
|
|
if go_idx != -1: sng_go_option.selected = go_idx
|
|
|
|
var stop_idx = [3, 4, 5].find(LobbyManager.sng_stop_duration)
|
|
if stop_idx != -1: sng_stop_option.selected = stop_idx
|
|
|
|
var goals_idx = [5, 8, 12].find(LobbyManager.sng_required_goals)
|
|
if goals_idx != -1: sng_goals_option.selected = goals_idx
|
|
|
|
func _on_doors_update(_val: int = 0) -> void:
|
|
if not doors_swap_option: return
|
|
|
|
var swap_idx = [10, 15, 30].find(LobbyManager.doors_swap_time)
|
|
if swap_idx != -1: doors_swap_option.selected = swap_idx
|
|
|
|
var refresh_idx = [15, 25, 40].find(LobbyManager.doors_refresh_time)
|
|
if refresh_idx != -1: doors_refresh_option.selected = refresh_idx
|
|
|
|
var goals_idx = [5, 8, 12].find(LobbyManager.doors_required_goals)
|
|
if goals_idx != -1: doors_goals_option.selected = goals_idx
|
|
|
|
func _update_random_spawn_label(enabled: bool) -> void:
|
|
if random_spawn_label:
|
|
random_spawn_label.text = "Random ✓" if enabled else "Random ✗"
|
|
|
|
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()
|
|
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)
|
|
|
|
if profile_panel_instance:
|
|
if main_menu_panel:
|
|
main_menu_panel.hide()
|
|
profile_panel_instance.show_panel()
|
|
|
|
func _on_logout_pressed() -> void:
|
|
AuthManager.logout()
|
|
_go_to_login()
|
|
|
|
func _on_quit_pressed() -> void:
|
|
print("[Lobby] Quitting game...")
|
|
get_tree().quit()
|
|
|
|
func _on_settings_pressed():
|
|
var settings_menu = get_node_or_null("SettingsMenu")
|
|
if not settings_menu:
|
|
var scene = load("res://scenes/ui/settings_menu.tscn")
|
|
if scene:
|
|
settings_menu = scene.instantiate()
|
|
settings_menu.name = "SettingsMenu"
|
|
add_child(settings_menu)
|
|
|
|
# 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:
|
|
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
|
|
|
|
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_panel_instance.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")
|
|
if leaderboard_panel_scene:
|
|
leaderboard_panel_instance = leaderboard_panel_scene.instantiate()
|
|
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:
|
|
if get_tree():
|
|
get_tree().change_scene_to_file("res://scenes/ui/login_screen.tscn")
|
|
|
|
# =============================================================================
|
|
# LobbyManager Signal Handlers
|
|
# =============================================================================
|
|
|
|
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"
|
|
else:
|
|
connection_status.text = "Found %d room(s)" % rooms.size()
|
|
|
|
func _on_room_joined(room_data: Dictionary) -> void:
|
|
_show_panel("lobby")
|
|
current_match_id = room_data.get("match_id", "")
|
|
match_id_display.text = "ID: %s" % _truncate_id(current_match_id)
|
|
|
|
# Configure host-specific UI
|
|
var is_host = LobbyManager.is_host
|
|
host_banner.visible = is_host
|
|
start_game_btn.visible = is_host
|
|
|
|
_update_settings_visibility()
|
|
|
|
# Update values from LobbyManager
|
|
_on_match_duration_changed(LobbyManager.match_duration)
|
|
_on_randomize_spawn_changed(LobbyManager.randomize_spawn)
|
|
_on_enable_cycle_timer_changed(LobbyManager.enable_cycle_timer)
|
|
|
|
# Scarcity Update
|
|
_on_scarcity_mode_changed(LobbyManager.scarcity_mode)
|
|
|
|
# Initial UI sync for custom modes
|
|
_on_sng_update()
|
|
_on_doors_update()
|
|
|
|
# Area selector: only host can interact
|
|
area_left_btn.disabled = not is_host
|
|
area_right_btn.disabled = not is_host
|
|
area_name_label.text = LobbyManager.get_selected_area()
|
|
|
|
# Game Mode Update
|
|
if game_mode_option: game_mode_option.visible = is_host
|
|
if game_mode_text_label: game_mode_text_label.visible = not is_host
|
|
_on_game_mode_changed(LobbyManager.game_mode)
|
|
|
|
_update_player_slots()
|
|
connection_status.text = "Connected to room"
|
|
|
|
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
|
|
connection_status.text = "Host disconnected. Returning to menu..."
|
|
_show_panel("main_menu")
|
|
|
|
func _on_player_joined(player_data: Dictionary) -> void:
|
|
_update_player_slots()
|
|
status_label.text = "%s joined!" % player_data.get("name", "Player")
|
|
|
|
func _on_player_left(_player_id: int) -> void:
|
|
_update_player_slots()
|
|
status_label.text = "A player left"
|
|
|
|
func _on_ready_state_changed(_player_id: int, _is_ready: bool) -> void:
|
|
_update_player_slots()
|
|
_update_status()
|
|
|
|
func _on_all_players_ready() -> void:
|
|
if LobbyManager.is_host:
|
|
start_game_btn.disabled = false
|
|
status_label.text = "All ready! Start the match!"
|
|
else:
|
|
status_label.text = "All ready! Waiting for host..."
|
|
|
|
func _on_game_starting() -> void:
|
|
connection_status.text = "Starting game..."
|
|
|
|
# Instantiate and use the loading screen
|
|
var loading_screen_scene = load("res://scenes/loading_screen/loading_screen.tscn")
|
|
if loading_screen_scene:
|
|
var loading_screen = loading_screen_scene.instantiate()
|
|
get_tree().root.add_child(loading_screen)
|
|
loading_screen.load_level("res://scenes/main.tscn")
|
|
else:
|
|
# Fallback if loading screen fails to load
|
|
get_tree().change_scene_to_file("res://scenes/main.tscn")
|
|
|
|
func _on_match_duration_changed(duration_seconds: int) -> void:
|
|
if not LobbyManager.is_host:
|
|
_update_duration_text_label(duration_seconds)
|
|
|
|
func _on_randomize_spawn_changed(enabled: bool) -> void:
|
|
if random_spawn_check:
|
|
random_spawn_check.set_pressed_no_signal(enabled)
|
|
if random_spawn_label:
|
|
random_spawn_label.text = "Random \u2713" if enabled else "Random \u2717"
|
|
|
|
func _on_enable_cycle_timer_changed(enabled: bool) -> void:
|
|
if enable_timer_check:
|
|
enable_timer_check.set_pressed_no_signal(enabled)
|
|
if enable_timer_label:
|
|
enable_timer_label.text = "Timer \u2713" if enabled else "Timer \u2717"
|
|
|
|
func _on_character_changed(_player_id: int, _character_name: String) -> void:
|
|
_update_player_slots()
|
|
|
|
func _on_area_changed(area_name: String) -> void:
|
|
area_name_label.text = area_name
|
|
|
|
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
|
|
_show_panel("main_menu")
|
|
|
|
func _on_profile_updated() -> void:
|
|
"""Handle profile updates (name/avatar change)."""
|
|
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
|
|
|
|
if username_label:
|
|
username_label.text = display_name
|
|
|
|
if avatar_display:
|
|
var avatar_url = UserProfileManager.get_avatar_url()
|
|
if ResourceLoader.exists(avatar_url):
|
|
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
|
|
# =============================================================================
|
|
|
|
func _update_player_slots() -> void:
|
|
"""Update all player slot visuals based on current player list."""
|
|
if not multiplayer.has_multiplayer_peer():
|
|
return
|
|
|
|
var players = LobbyManager.get_players()
|
|
var my_id = multiplayer.get_unique_id()
|
|
|
|
for i in range(player_slots.size()):
|
|
var slot = player_slots[i]
|
|
var slot_num = i + 1
|
|
|
|
if i < players.size():
|
|
var player = players[i]
|
|
slot.visible = true
|
|
|
|
# Update player name
|
|
var name_label = slot.get_node_or_null("PlayerName%d" % slot_num)
|
|
if name_label:
|
|
var display_name = player.get("name", "Player %d" % slot_num)
|
|
if player.get("id") == 1:
|
|
display_name += " (Host)"
|
|
name_label.text = display_name
|
|
|
|
# Update character preview
|
|
var char_preview = slot.get_node_or_null("CharacterPreview%d" % slot_num)
|
|
var char_name = player.get("character", "Bob")
|
|
if char_preview and character_textures.has(char_name):
|
|
char_preview.texture = character_textures[char_name]
|
|
|
|
# Check if this is the local player
|
|
var is_local_player = player.get("id") == my_id
|
|
|
|
# Update character name in nav (inside CharacterNav)
|
|
var char_name_in_nav = slot.get_node_or_null("CharacterNav%d/CharacterName%d" % [slot_num, slot_num])
|
|
if char_name_in_nav:
|
|
char_name_in_nav.text = char_name
|
|
|
|
# Update character name label (outside nav, for non-local players)
|
|
var char_name_label = slot.get_node_or_null("CharacterNameLabel%d" % slot_num)
|
|
if char_name_label:
|
|
char_name_label.text = char_name
|
|
char_name_label.visible = not is_local_player
|
|
|
|
# Show/hide character navigation (only for local player)
|
|
var char_nav = slot.get_node_or_null("CharacterNav%d" % slot_num)
|
|
if char_nav:
|
|
char_nav.visible = is_local_player
|
|
|
|
# Update ready status
|
|
var ready_label = slot.get_node_or_null("ReadyStatus%d" % slot_num)
|
|
if ready_label:
|
|
var is_ready = player.get("is_ready", false)
|
|
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))
|
|
else:
|
|
# Empty slot - show as bot placeholder
|
|
slot.visible = true
|
|
|
|
# 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_display_name
|
|
|
|
# Use a character for bot preview
|
|
var char_preview = slot.get_node_or_null("CharacterPreview%d" % slot_num)
|
|
var bot_characters = ["Copper", "Dabro", "Gatot", "Pip"]
|
|
var bot_char = bot_characters[(i) % bot_characters.size()]
|
|
if char_preview and character_textures.has(bot_char):
|
|
char_preview.texture = character_textures[bot_char]
|
|
|
|
# Hide character navigation for bots
|
|
var char_nav = slot.get_node_or_null("CharacterNav%d" % slot_num)
|
|
if char_nav:
|
|
char_nav.visible = false
|
|
|
|
# Update character name label
|
|
var char_name_label = slot.get_node_or_null("CharacterNameLabel%d" % slot_num)
|
|
if char_name_label:
|
|
char_name_label.text = bot_char
|
|
char_name_label.visible = true
|
|
|
|
# Show ready status as waiting
|
|
var ready_label = slot.get_node_or_null("ReadyStatus%d" % slot_num)
|
|
if ready_label:
|
|
ready_label.text = "WAITING..."
|
|
ready_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.7))
|
|
|
|
_update_status()
|
|
|
|
func _update_status() -> void:
|
|
var players = LobbyManager.get_players()
|
|
var ready_count = 0
|
|
for player in players:
|
|
if player.get("is_ready", false):
|
|
ready_count += 1
|
|
|
|
status_label.text = "Ready: %d/%d" % [ready_count, players.size()]
|
|
|
|
if LobbyManager.is_host:
|
|
start_game_btn.disabled = not LobbyManager.is_all_ready()
|
|
|
|
# =============================================================================
|
|
# Helper Functions
|
|
# =============================================================================
|
|
|
|
func _update_duration_text_label(duration_seconds: int) -> void:
|
|
var duration_text: String
|
|
match duration_seconds:
|
|
60: duration_text = "1 min"
|
|
120: duration_text = "2 min"
|
|
180: duration_text = "3 min"
|
|
300: duration_text = "5 min"
|
|
600: duration_text = "10 min"
|
|
_: duration_text = "%d sec" % duration_seconds
|
|
duration_text_label.text = duration_text
|
|
|
|
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)
|
|
|
|
|
|
# =============================================================================
|
|
# 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
|