chore: release version 2.3.5 and refactor lobby

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.
This commit is contained in:
2026-05-22 12:08:11 +08:00
parent 8430d1054e
commit decdb74ade
356 changed files with 27438 additions and 1630 deletions
+427
View File
@@ -0,0 +1,427 @@
class_name LobbyRoom
extends RefCounted
var lobby: Control
var _bot_names: Dictionary = {}
func _init(p_lobby: Control):
lobby = p_lobby
if lobby.copy_id_btn:
lobby.copy_id_btn.pressed.connect(_on_copy_id_pressed)
if lobby.duration_option:
lobby.duration_option.item_selected.connect(_on_duration_selected)
if lobby.random_spawn_check:
lobby.random_spawn_check.toggled.connect(_on_random_spawn_toggled)
if lobby.enable_timer_check:
lobby.enable_timer_check.toggled.connect(_on_enable_timer_toggled)
if lobby.area_left_btn:
lobby.area_left_btn.pressed.connect(func(): LobbyManager.cycle_area(-1))
if lobby.area_right_btn:
lobby.area_right_btn.pressed.connect(func(): LobbyManager.cycle_area(1))
if lobby.leave_btn:
lobby.leave_btn.pressed.connect(_on_leave_pressed)
if lobby.ready_btn:
lobby.ready_btn.toggled.connect(_on_ready_toggled)
if lobby.start_game_btn:
lobby.start_game_btn.pressed.connect(_on_start_game_pressed)
if lobby.scarcity_option:
lobby.scarcity_option.item_selected.connect(_on_scarcity_selected)
if lobby.game_mode_option:
lobby.game_mode_option.item_selected.connect(_on_game_mode_selected)
# Connect LobbyManager signals
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)
FriendManager.lobby_invite_received.connect(_on_lobby_invite_received)
# =============================================================================
# Button Handlers
# =============================================================================
func _on_ready_toggled(is_ready: bool) -> void:
LobbyManager.set_ready(is_ready)
if lobby.ready_btn:
lobby.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()
lobby._show_panel("main_menu")
if lobby.ready_btn:
lobby.ready_btn.button_pressed = false
lobby.ready_btn.text = "READY"
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(lobby.current_match_id)
if lobby.status_label:
lobby.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 = lobby.scarcity_option.get_item_text(index)
LobbyManager.set_scarcity_mode(mode)
func _on_scarcity_mode_changed(mode: String) -> void:
if lobby.scarcity_option:
for i in range(lobby.scarcity_option.item_count):
if lobby.scarcity_option.get_item_text(i) == mode:
lobby.scarcity_option.selected = i
break
if lobby.scarcity_label:
lobby.scarcity_label.text = mode
func _on_game_mode_selected(index: int) -> void:
if not LobbyManager.is_host: return
var mode = lobby.game_mode_option.get_item_text(index)
LobbyManager.set_game_mode(mode)
func _on_game_mode_changed(mode: String) -> void:
if lobby.game_mode_option:
for i in range(lobby.game_mode_option.item_count):
if lobby.game_mode_option.get_item_text(i) == mode:
lobby.game_mode_option.selected = i
break
if lobby.game_mode_text_label:
lobby.game_mode_text_label.text = mode
lobby._update_settings_visibility()
func _on_sng_update(_val: int = 0) -> void:
if not lobby.sng_go_option: return
var go_idx = [10, 15, 25].find(LobbyManager.sng_go_duration)
if go_idx != -1: lobby.sng_go_option.selected = go_idx
var stop_idx = [3, 4, 5].find(LobbyManager.sng_stop_duration)
if stop_idx != -1: lobby.sng_stop_option.selected = stop_idx
var goals_idx = [5, 8, 12].find(LobbyManager.sng_required_goals)
if goals_idx != -1: lobby.sng_goals_option.selected = goals_idx
func _on_doors_update(_val: int = 0) -> void:
if not lobby.doors_swap_option: return
var swap_idx = [10, 15, 30].find(LobbyManager.doors_swap_time)
if swap_idx != -1: lobby.doors_swap_option.selected = swap_idx
var refresh_idx = [15, 25, 40].find(LobbyManager.doors_refresh_time)
if refresh_idx != -1: lobby.doors_refresh_option.selected = refresh_idx
var goals_idx = [5, 8, 12].find(LobbyManager.doors_required_goals)
if goals_idx != -1: lobby.doors_goals_option.selected = goals_idx
# =============================================================================
# LobbyManager Signal Handlers
# =============================================================================
func _on_room_joined(room_data: Dictionary) -> void:
lobby._show_panel("lobby")
lobby.current_match_id = room_data.get("match_id", "")
if lobby.match_id_display:
lobby.match_id_display.text = "ID: %s" % _truncate_id(lobby.current_match_id)
var is_host = LobbyManager.is_host
if lobby.host_banner: lobby.host_banner.visible = is_host
if lobby.start_game_btn: lobby.start_game_btn.visible = is_host
lobby._update_settings_visibility()
_on_match_duration_changed(LobbyManager.match_duration)
_on_randomize_spawn_changed(LobbyManager.randomize_spawn)
_on_enable_cycle_timer_changed(LobbyManager.enable_cycle_timer)
_on_scarcity_mode_changed(LobbyManager.scarcity_mode)
_on_sng_update()
_on_doors_update()
if lobby.area_left_btn: lobby.area_left_btn.disabled = not is_host
if lobby.area_right_btn: lobby.area_right_btn.disabled = not is_host
if lobby.area_name_label: lobby.area_name_label.text = LobbyManager.get_selected_area()
if lobby.game_mode_option: lobby.game_mode_option.visible = is_host
if lobby.game_mode_text_label: lobby.game_mode_text_label.visible = not is_host
_on_game_mode_changed(LobbyManager.game_mode)
_update_player_slots()
if lobby.connection_status: lobby.connection_status.text = "Connected to room"
func _on_room_left() -> void:
lobby._show_panel("main_menu")
if lobby.connection_status: lobby.connection_status.text = "Left room"
for key in _bot_names.keys():
NameGenerator.release_bot_name(_bot_names[key])
_bot_names.clear()
func _on_host_disconnected() -> void:
if lobby.connection_status: lobby.connection_status.text = "Host disconnected. Returning to menu..."
lobby._show_panel("main_menu")
func _on_player_joined(player_data: Dictionary) -> void:
_update_player_slots()
if lobby.status_label: lobby.status_label.text = "%s joined!" % player_data.get("name", "Player")
func _on_player_left(_player_id: int) -> void:
_update_player_slots()
if lobby.status_label: lobby.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:
if lobby.start_game_btn: lobby.start_game_btn.disabled = false
if lobby.status_label: lobby.status_label.text = "All ready! Start the match!"
else:
if lobby.status_label: lobby.status_label.text = "All ready! Waiting for host..."
func _on_game_starting() -> void:
if lobby.connection_status: lobby.connection_status.text = "Starting game..."
var loading_screen_scene = load("res://scenes/loading_screen/loading_screen.tscn")
if loading_screen_scene:
var loading_screen = loading_screen_scene.instantiate()
lobby.get_tree().root.add_child(loading_screen)
loading_screen.load_level("res://scenes/main.tscn")
else:
lobby.get_tree().change_scene_to_file("res://scenes/main.tscn")
func _on_match_duration_changed(duration_seconds: int) -> void:
if not LobbyManager.is_host:
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
if lobby.duration_text_label: lobby.duration_text_label.text = duration_text
func _on_randomize_spawn_changed(enabled: bool) -> void:
if lobby.random_spawn_check:
lobby.random_spawn_check.set_pressed_no_signal(enabled)
if lobby.random_spawn_label:
lobby.random_spawn_label.text = "Random \u2713" if enabled else "Random \u2717"
func _on_enable_cycle_timer_changed(enabled: bool) -> void:
if lobby.enable_timer_check:
lobby.enable_timer_check.set_pressed_no_signal(enabled)
if lobby.enable_timer_label:
lobby.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:
if lobby.area_name_label: lobby.area_name_label.text = area_name
func _update_player_slots() -> void:
if not lobby.multiplayer.has_multiplayer_peer():
return
var players = LobbyManager.get_players()
var my_id = lobby.multiplayer.get_unique_id()
for i in range(lobby.player_slots.size()):
var slot = lobby.player_slots[i]
var slot_num = i + 1
if i < players.size():
var player = players[i]
slot.visible = true
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
var char_preview = slot.get_node_or_null("CharacterPreview%d" % slot_num)
var char_name = player.get("character", "Bob")
if char_preview and lobby.character_textures.has(char_name):
char_preview.texture = lobby.character_textures[char_name]
var is_local_player = player.get("id") == my_id
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
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
var char_nav = slot.get_node_or_null("CharacterNav%d" % slot_num)
if char_nav:
char_nav.visible = is_local_player
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))
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:
slot.visible = true
if not _bot_names.has(i):
_bot_names[i] = NameGenerator.generate_bot_name()
var bot_display_name: String = _bot_names[i]
var name_label = slot.get_node_or_null("PlayerName%d" % slot_num)
if name_label:
name_label.text = "🤖 " + bot_display_name
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 lobby.character_textures.has(bot_char):
char_preview.texture = lobby.character_textures[bot_char]
var char_nav = slot.get_node_or_null("CharacterNav%d" % slot_num)
if char_nav:
char_nav.visible = false
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
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))
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()
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
if lobby.status_label:
lobby.status_label.text = "Ready: %d/%d" % [ready_count, players.size()]
if LobbyManager.is_host:
if lobby.start_game_btn: lobby.start_game_btn.disabled = not LobbyManager.is_all_ready()
func _truncate_id(id: String) -> String:
if id.length() > 16:
return id.substr(0, 8) + "..." + id.substr(-4)
return id
# =============================================================================
# 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:
var match_id = lobby.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()
lobby.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:
if lobby.get_tree().current_scene.scene_file_path != "res://scenes/lobby.tscn":
return
if lobby.lobby_panel and lobby.lobby_panel.visible:
return
if lobby._invite_popup:
lobby._invite_popup.queue_free()
lobby._pending_invite_match_id = match_id
var scene = load("res://scenes/ui/lobby_invite_popup.tscn") as PackedScene
if scene:
lobby._invite_popup = scene.instantiate()
lobby.add_child(lobby._invite_popup)
lobby._invite_popup.setup(from_name)
lobby._invite_popup.accepted.connect(_on_invite_accepted)
lobby._invite_popup.declined.connect(func(): lobby._invite_popup.queue_free())
lobby._invite_popup.popup_centered()
else:
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")
lobby.add_child(dlg)
dlg.confirmed.connect(_on_invite_accepted)
dlg.canceled.connect(dlg.queue_free)
dlg.popup_centered()
lobby._invite_popup = dlg
func _on_invite_accepted() -> void:
if not lobby._pending_invite_match_id.is_empty():
LobbyManager.join_room(lobby._pending_invite_match_id)
if lobby._invite_popup:
lobby._invite_popup.queue_free()
lobby._pending_invite_match_id = ""