595 lines
23 KiB
GDScript
595 lines
23 KiB
GDScript
extends Control
|
|
## Main menu with login/register UI wired to the backend auth API.
|
|
##
|
|
## The AuthPanel is hidden by default. When the user clicks the LoginButton or
|
|
## RegisterButton (AuthButtonsRow, above the panel), the auth panel slides in.
|
|
## The Login and Register buttons on the AuthButtonsRow directly set the panel's
|
|
## mode, so the user can switch between Login and Register at any time while
|
|
## the panel is open.
|
|
##
|
|
## On successful auth the token is stored via ApiClient, AuthButtonsRow hides,
|
|
## the welcome label shows the username, and the Play button becomes enabled.
|
|
## The auth form fields are hidden, leaving only the welcome message visible.
|
|
##
|
|
## When returning from another scene (e.g. world_list), _ready() checks whether
|
|
## ApiClient already holds a valid token. If so, it restores the authenticated
|
|
## UI state (Play enabled, welcome message visible) instead of forcing the user
|
|
## back to the login form.
|
|
|
|
|
|
## Emitted once _ready()'s session check (has_token() + the awaited
|
|
## get_me() call) has actually resolved, whichever way -- lets a test (or
|
|
## anything else) know the UI state is trustworthy now, rather than
|
|
## guessing from a frame count or from UI properties that are ambiguous
|
|
## mid-await (e.g. AuthButtonsRow.visible is true both before the check
|
|
## resolves AND in a genuine logged-out end state).
|
|
signal auth_state_ready
|
|
|
|
# ─── UI references ────────────────────────────────────────────────────────────
|
|
|
|
var _auth_buttons_row: HBoxContainer
|
|
var _login_button: Button
|
|
var _register_button: Button
|
|
var _auth_panel: PanelContainer
|
|
var _auth_mode_label: Label
|
|
var _username_edit: LineEdit
|
|
var _password_edit: LineEdit
|
|
var _email_edit: LineEdit
|
|
var _over_18_check: CheckBox
|
|
var _auth_button: Button
|
|
var _status_label: Label
|
|
var _welcome_label: Label
|
|
var _discover_button: Button
|
|
var _play_button: Button
|
|
var _customize_button: Button
|
|
var _social_button: Button
|
|
var _chat_button: Button
|
|
var _marketplace_button: Button
|
|
var _wallet_button: Button
|
|
var _groups_button: Button
|
|
var _selling_button: Button
|
|
var _quit_button: Button
|
|
|
|
var _is_register_mode: bool = false
|
|
var _is_authenticating: bool = false
|
|
var _sign_out_button: Button = null
|
|
|
|
|
|
# ─── Lifecycle ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
static func _build_label() -> String:
|
|
var f := FileAccess.open("res://assets/build_info.json", FileAccess.READ)
|
|
if f == null:
|
|
return "dev"
|
|
var d = JSON.parse_string(f.get_as_text())
|
|
if d is Dictionary:
|
|
if d.has("build_label"):
|
|
var c: String = str(d.get("commit", ""))
|
|
if not c.is_empty():
|
|
return "%s (%s)" % [d.get("build_label"), c]
|
|
return str(d.get("build_label"))
|
|
var v: String = str(d.get("version", ""))
|
|
if not v.is_empty():
|
|
return "MVP Build %s (%s)" % [v, d.get("commit", "?")]
|
|
return "%s %s" % [d.get("checkpoint", "?"), d.get("commit", "?")]
|
|
return "dev"
|
|
|
|
|
|
static func _update_fs_button(btn: Button) -> void:
|
|
if DisplayServer.get_name() == "headless":
|
|
btn.text = "⛶ Fullscreen (F11)"
|
|
return
|
|
var m := DisplayServer.window_get_mode()
|
|
if m == DisplayServer.WINDOW_MODE_FULLSCREEN or m == DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN:
|
|
btn.text = "🗗 Windowed (F11)"
|
|
else:
|
|
btn.text = "⛶ Fullscreen (F11)"
|
|
|
|
|
|
static func _update_sound_button(btn: Button) -> void:
|
|
if btn == null or not is_instance_valid(btn):
|
|
return
|
|
var main_loop = Engine.get_main_loop()
|
|
var audio_mgr: Node = main_loop.root.get_node_or_null("AudioManager") if main_loop is SceneTree else null
|
|
var enabled: bool = audio_mgr.is_sound_enabled() if audio_mgr and audio_mgr.has_method("is_sound_enabled") else true
|
|
if enabled:
|
|
btn.text = "🔊 Sound: ON [M]"
|
|
btn.tooltip_text = "Sound is enabled. Click or press [M] to mute."
|
|
else:
|
|
btn.text = "🔇 Sound: OFF [M]"
|
|
btn.tooltip_text = "Sound is muted. Click or press [M] to unmute."
|
|
|
|
|
|
func _ready() -> void:
|
|
for arg in OS.get_cmdline_args():
|
|
if arg == "--studio" or arg == "-studio":
|
|
get_tree().change_scene_to_file("res://scenes/studio/studio_main.tscn")
|
|
return
|
|
|
|
var v := Label.new()
|
|
var bl := _build_label()
|
|
v.text = bl if bl.begins_with("MVP Build") or bl == "dev" else ("build %s" % bl)
|
|
v.add_theme_color_override("font_color", Color(1, 1, 1, 0.7))
|
|
v.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
|
|
v.add_theme_constant_override("outline_size", 4)
|
|
v.add_theme_font_size_override("font_size", 12)
|
|
# Full-width strip pinned to the bottom, text right-aligned. A bare
|
|
# BOTTOM_RIGHT preset + .position on a zero-size Label collapses the rect
|
|
# and nothing renders -- which is why this never showed up.
|
|
v.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
|
|
v.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
v.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
|
v.offset_left = 8
|
|
v.offset_right = -10
|
|
v.offset_top = -28
|
|
v.offset_bottom = -6
|
|
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
add_child(v)
|
|
|
|
var fs_btn := Button.new()
|
|
fs_btn.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
|
fs_btn.offset_left = -160
|
|
fs_btn.offset_top = 10
|
|
fs_btn.offset_right = -10
|
|
fs_btn.offset_bottom = 38
|
|
fs_btn.add_theme_font_size_override("font_size", 12)
|
|
_update_fs_button(fs_btn)
|
|
fs_btn.pressed.connect(func():
|
|
Session.toggle_fullscreen()
|
|
_update_fs_button(fs_btn)
|
|
)
|
|
add_child(fs_btn)
|
|
|
|
var ctrl_btn := Button.new()
|
|
ctrl_btn.name = "ControlsButton"
|
|
ctrl_btn.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
|
ctrl_btn.offset_left = -300
|
|
ctrl_btn.offset_top = 10
|
|
ctrl_btn.offset_right = -170
|
|
ctrl_btn.offset_bottom = 38
|
|
ctrl_btn.text = "⌨️ Controls"
|
|
ctrl_btn.add_theme_font_size_override("font_size", 12)
|
|
ctrl_btn.pressed.connect(_on_controls_button_pressed)
|
|
add_child(ctrl_btn)
|
|
|
|
var sound_btn := Button.new()
|
|
sound_btn.name = "SoundButton"
|
|
sound_btn.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
|
sound_btn.offset_left = -460
|
|
sound_btn.offset_top = 10
|
|
sound_btn.offset_right = -310
|
|
sound_btn.offset_bottom = 38
|
|
sound_btn.add_theme_font_size_override("font_size", 12)
|
|
_update_sound_button(sound_btn)
|
|
sound_btn.pressed.connect(func():
|
|
var audio_mgr = get_node_or_null("/root/AudioManager")
|
|
if audio_mgr and audio_mgr.has_method("toggle_sound"):
|
|
audio_mgr.toggle_sound()
|
|
_update_sound_button(sound_btn)
|
|
)
|
|
add_child(sound_btn)
|
|
|
|
# Cache node references.
|
|
_auth_buttons_row = $CenterContainer/VBoxContainer/AuthButtonsRow
|
|
_login_button = $CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton
|
|
_register_button = $CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton
|
|
_auth_panel = $CenterContainer/VBoxContainer/AuthPanel
|
|
|
|
# Prominent sound button in AuthButtonsRow on the logged-out start screen
|
|
var auth_sound_btn := Button.new()
|
|
auth_sound_btn.name = "AuthSoundButton"
|
|
auth_sound_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
auth_sound_btn.add_theme_font_size_override("font_size", 13)
|
|
_update_sound_button(auth_sound_btn)
|
|
auth_sound_btn.pressed.connect(func():
|
|
var audio_mgr = get_node_or_null("/root/AudioManager")
|
|
if audio_mgr and audio_mgr.has_method("toggle_sound"):
|
|
audio_mgr.toggle_sound()
|
|
_update_sound_button(auth_sound_btn)
|
|
)
|
|
_auth_buttons_row.add_child(auth_sound_btn)
|
|
|
|
# Sound toggle also inside AuthPanel so user can toggle sound without closing the auth form
|
|
var panel_sound_btn := Button.new()
|
|
panel_sound_btn.name = "PanelSoundButton"
|
|
panel_sound_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
panel_sound_btn.add_theme_font_size_override("font_size", 12)
|
|
_update_sound_button(panel_sound_btn)
|
|
panel_sound_btn.pressed.connect(func():
|
|
var audio_mgr = get_node_or_null("/root/AudioManager")
|
|
if audio_mgr and audio_mgr.has_method("toggle_sound"):
|
|
audio_mgr.toggle_sound()
|
|
_update_sound_button(panel_sound_btn)
|
|
)
|
|
var auth_vbox = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox
|
|
auth_vbox.add_child(panel_sound_btn)
|
|
|
|
var audio_singleton = get_node_or_null("/root/AudioManager")
|
|
if audio_singleton and audio_singleton.has_signal("sound_toggled"):
|
|
var on_toggled = func(_enabled: bool):
|
|
if is_instance_valid(sound_btn):
|
|
_update_sound_button(sound_btn)
|
|
if is_instance_valid(auth_sound_btn):
|
|
_update_sound_button(auth_sound_btn)
|
|
if is_instance_valid(panel_sound_btn):
|
|
_update_sound_button(panel_sound_btn)
|
|
audio_singleton.sound_toggled.connect(on_toggled)
|
|
tree_exiting.connect(func():
|
|
if is_instance_valid(audio_singleton) and audio_singleton.sound_toggled.is_connected(on_toggled):
|
|
audio_singleton.sound_toggled.disconnect(on_toggled)
|
|
)
|
|
_auth_mode_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel
|
|
_username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit
|
|
_password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit
|
|
_email_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit
|
|
if _email_edit and not _email_edit.text_submitted.is_connected(_on_auth_button_pressed):
|
|
_email_edit.text_submitted.connect(_on_auth_button_pressed)
|
|
_over_18_check = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/Over18Check
|
|
_auth_button = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton
|
|
_status_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthStatusLabel
|
|
_welcome_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel
|
|
_discover_button = $CenterContainer/VBoxContainer.get_node_or_null("DiscoverButton")
|
|
_play_button = $CenterContainer/VBoxContainer/PlayButton
|
|
_customize_button = $CenterContainer/VBoxContainer/CustomizeButton
|
|
_social_button = $CenterContainer/VBoxContainer.get_node_or_null("SocialButton")
|
|
_chat_button = $CenterContainer/VBoxContainer.get_node_or_null("ChatButton")
|
|
_marketplace_button = $CenterContainer/VBoxContainer.get_node_or_null("MarketplaceButton")
|
|
_wallet_button = $CenterContainer/VBoxContainer.get_node_or_null("WalletButton")
|
|
_groups_button = $CenterContainer/VBoxContainer.get_node_or_null("GroupsButton")
|
|
_selling_button = $CenterContainer/VBoxContainer.get_node_or_null("SellingButton")
|
|
_quit_button = $CenterContainer/VBoxContainer/QuitButton
|
|
|
|
var studio_btn := Button.new()
|
|
studio_btn.name = "StudioButton"
|
|
studio_btn.text = "🦬 ModBison Studio"
|
|
studio_btn.add_theme_color_override("font_color", Color(1.0, 0.9, 0.35))
|
|
studio_btn.pressed.connect(func():
|
|
get_tree().change_scene_to_file("res://scenes/studio/studio_main.tscn")
|
|
)
|
|
var vb = $CenterContainer/VBoxContainer
|
|
if vb != null:
|
|
vb.add_child(studio_btn)
|
|
if _play_button != null:
|
|
vb.move_child(studio_btn, _play_button.get_index() + 1)
|
|
|
|
# Default to navigation being gated. When a token exists, the async session
|
|
# check below may take several frames (a real get_me() round-trip);
|
|
# keeping the button disabled until validation succeeds prevents a fast
|
|
# click from navigating to world_list with an expired/invalid token.
|
|
_set_nav_enabled(false)
|
|
|
|
# Check whether a valid session already exists (e.g. user logged in,
|
|
# navigated to world_list, then clicked Back). If so, restore the
|
|
# authenticated UI state instead of forcing the login form again.
|
|
if ApiClient.has_token():
|
|
var me_result: Dictionary = await ApiClient.get_me()
|
|
if me_result.ok and me_result.data is Dictionary:
|
|
var username: String = str((me_result.data as Dictionary).get("username", ""))
|
|
if username != "":
|
|
_show_authenticated(username)
|
|
await _prime_avatar()
|
|
auth_state_ready.emit()
|
|
return
|
|
|
|
# Token missing or invalid — fall through to logged-out state.
|
|
_reset_to_logged_out()
|
|
auth_state_ready.emit()
|
|
|
|
|
|
func _set_nav_enabled(enabled: bool) -> void:
|
|
var buttons = [
|
|
_discover_button, _play_button, _customize_button, _social_button,
|
|
_chat_button, _marketplace_button, _wallet_button, _groups_button, _selling_button
|
|
]
|
|
for b in buttons:
|
|
if b != null and is_instance_valid(b):
|
|
b.disabled = not enabled
|
|
|
|
|
|
## Restore the UI to the authenticated state (Play enabled, welcome message
|
|
## visible, auth buttons hidden). AuthPanel itself stays visible -- the
|
|
## welcome label lives inside it, so hiding the panel hides the label too
|
|
## even when the label's own `visible` is true (real bug, 2026-08-20: a
|
|
## review caught the panel being hidden here while the label claimed to be
|
|
## visible, which is a no-op UI-wise since Control.visible doesn't override
|
|
## an ancestor's hidden state).
|
|
func _show_authenticated(username: String) -> void:
|
|
_auth_panel.visible = true
|
|
_status_label.visible = false
|
|
_welcome_label.text = "Welcome, %s!" % username
|
|
_welcome_label.visible = true
|
|
# Hide the same full set of form fields _on_auth_complete() hides right
|
|
# after a fresh login/register -- this path (session restore on scene
|
|
# reload, e.g. after Play -> Back) used to only hide EmailEdit/
|
|
# Over18Check, leaving UsernameEdit/PasswordEdit/AuthButton/
|
|
# AuthModeLabel visible on top of the welcome message. Real bug: Play
|
|
# was enabled and the welcome label was correct, so it wasn't caught by
|
|
# checks for those alone -- the login fields being back on screen is
|
|
# what a user actually sees as "it's making me log in again."
|
|
_username_edit.visible = false
|
|
_password_edit.visible = false
|
|
_email_edit.visible = false
|
|
_over_18_check.visible = false
|
|
_auth_button.visible = false
|
|
_auth_mode_label.visible = false
|
|
_set_nav_enabled(true)
|
|
_auth_buttons_row.visible = false
|
|
if _sign_out_button == null or not is_instance_valid(_sign_out_button):
|
|
_sign_out_button = Button.new()
|
|
_sign_out_button.name = "SignOutButton"
|
|
_sign_out_button.text = "Sign Out"
|
|
_sign_out_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
|
_sign_out_button.add_theme_font_size_override("font_size", 12)
|
|
_sign_out_button.pressed.connect(_on_sign_out_pressed)
|
|
$CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox.add_child(_sign_out_button)
|
|
_sign_out_button.visible = true
|
|
print("Restored authenticated session for: ", username)
|
|
|
|
|
|
## Reset the UI to the logged-out state (Play disabled, auth panel hidden,
|
|
## login/register buttons visible).
|
|
func _reset_to_logged_out() -> void:
|
|
_auth_panel.visible = false
|
|
_status_label.visible = false
|
|
_welcome_label.visible = false
|
|
if _sign_out_button != null and is_instance_valid(_sign_out_button):
|
|
_sign_out_button.visible = false
|
|
_email_edit.visible = false
|
|
_over_18_check.visible = false
|
|
_over_18_check.button_pressed = true
|
|
_set_nav_enabled(false)
|
|
_update_auth_mode_ui()
|
|
|
|
|
|
func _on_sign_out_pressed() -> void:
|
|
ApiClient.clear_token()
|
|
_reset_to_logged_out()
|
|
_auth_buttons_row.visible = true
|
|
_clear_status()
|
|
|
|
|
|
# ─── Navigation ───────────────────────────────────────────────────────────────
|
|
|
|
func _on_discover_button_pressed() -> void:
|
|
if _discover_button != null and _discover_button.disabled:
|
|
return
|
|
get_tree().change_scene_to_file("res://scenes/discovery_ui.tscn")
|
|
|
|
|
|
func _on_play_button_pressed() -> void:
|
|
if _play_button.disabled:
|
|
return
|
|
get_tree().change_scene_to_file("res://scenes/world_list.tscn")
|
|
|
|
|
|
func _on_customize_button_pressed() -> void:
|
|
if _customize_button.disabled:
|
|
return
|
|
get_tree().change_scene_to_file("res://scenes/avatar_customization.tscn")
|
|
|
|
|
|
func _on_social_button_pressed() -> void:
|
|
if _social_button != null and _social_button.disabled:
|
|
return
|
|
get_tree().change_scene_to_file("res://scenes/friends_presence_ui.tscn")
|
|
|
|
|
|
func _on_chat_button_pressed() -> void:
|
|
if _chat_button != null and _chat_button.disabled:
|
|
return
|
|
get_tree().change_scene_to_file("res://scenes/chat_ui.tscn")
|
|
|
|
|
|
func _on_marketplace_button_pressed() -> void:
|
|
if _marketplace_button != null and _marketplace_button.disabled:
|
|
return
|
|
get_tree().change_scene_to_file("res://scenes/marketplace_ui.tscn")
|
|
|
|
|
|
func _on_wallet_button_pressed() -> void:
|
|
if _wallet_button != null and _wallet_button.disabled:
|
|
return
|
|
get_tree().change_scene_to_file("res://scenes/wallet_ui.tscn")
|
|
|
|
|
|
func _on_groups_button_pressed() -> void:
|
|
if _groups_button != null and _groups_button.disabled:
|
|
return
|
|
get_tree().change_scene_to_file("res://scenes/groups_ui.tscn")
|
|
|
|
|
|
func _on_selling_button_pressed() -> void:
|
|
if _selling_button != null and _selling_button.disabled:
|
|
return
|
|
get_tree().change_scene_to_file("res://scenes/selling_payout_ui.tscn")
|
|
|
|
|
|
func _on_quit_button_pressed() -> void:
|
|
get_tree().quit()
|
|
|
|
|
|
|
|
|
|
|
|
# ─── Auth panel toggle ────────────────────────────────────────────────────────
|
|
|
|
func _on_login_button_pressed() -> void:
|
|
"""Show the auth panel in login mode, called from AuthButtonsRow/LoginButton."""
|
|
_auth_panel.visible = true
|
|
_is_register_mode = false
|
|
_update_auth_mode_ui()
|
|
_clear_status()
|
|
_username_edit.grab_focus()
|
|
|
|
|
|
func _on_register_button_pressed() -> void:
|
|
"""Show the auth panel in register mode, called from AuthButtonsRow/RegisterButton."""
|
|
_auth_panel.visible = true
|
|
_is_register_mode = true
|
|
_update_auth_mode_ui()
|
|
_clear_status()
|
|
_username_edit.grab_focus()
|
|
|
|
|
|
func _update_auth_mode_ui() -> void:
|
|
"""Update labels and field visibility based on login vs register mode."""
|
|
if _is_register_mode:
|
|
_auth_mode_label.text = "Register"
|
|
_auth_button.text = "Register"
|
|
_email_edit.visible = true
|
|
_over_18_check.visible = true
|
|
else:
|
|
_auth_mode_label.text = "Login"
|
|
_auth_button.text = "Login"
|
|
_email_edit.visible = false
|
|
_over_18_check.visible = false
|
|
|
|
|
|
# ─── Auth submission ──────────────────────────────────────────────────────────
|
|
|
|
func _on_auth_button_pressed(_text: String = "") -> void:
|
|
"""Handle login or register button press."""
|
|
if _is_authenticating:
|
|
return
|
|
|
|
# Validate inputs.
|
|
var username := _username_edit.text.strip_edges()
|
|
var password := _password_edit.text
|
|
|
|
if username == "":
|
|
_show_status("Username is required.")
|
|
return
|
|
if password == "":
|
|
_show_status("Password is required.")
|
|
return
|
|
|
|
if _is_register_mode:
|
|
var email := _email_edit.text.strip_edges()
|
|
|
|
if email == "":
|
|
_show_status("Email is required.")
|
|
return
|
|
if not _over_18_check.button_pressed:
|
|
_show_status("You must confirm you are 18 or older to register.")
|
|
return
|
|
|
|
_auth_button.disabled = true
|
|
_auth_button.text = "Registering..."
|
|
_is_authenticating = true
|
|
_clear_status()
|
|
|
|
var result := await ApiClient.register(username, email, password, true)
|
|
await _on_auth_complete(result)
|
|
else:
|
|
_auth_button.disabled = true
|
|
_auth_button.text = "Logging in..."
|
|
_is_authenticating = true
|
|
_clear_status()
|
|
|
|
var result := await ApiClient.login(username, password)
|
|
await _on_auth_complete(result)
|
|
|
|
|
|
func _on_auth_complete(result: Dictionary) -> void:
|
|
"""Handle the result of a login or register call."""
|
|
_auth_button.disabled = false
|
|
_auth_button.text = "Register" if _is_register_mode else "Login"
|
|
_is_authenticating = false
|
|
|
|
if result.ok:
|
|
# Update the UI to the authenticated state first, synchronously, so
|
|
# the player sees "Welcome" + an enabled Play immediately -- then
|
|
# prime the avatar (a catalog load + a loadout fetch) in the
|
|
# background. A slow or failing prime must never gate getting in.
|
|
_username_edit.visible = false
|
|
_password_edit.visible = false
|
|
_email_edit.visible = false
|
|
_over_18_check.visible = false
|
|
_auth_button.visible = false
|
|
_auth_mode_label.visible = false
|
|
_status_label.visible = false
|
|
_welcome_label.text = "Welcome, %s!" % _username_edit.text.strip_edges()
|
|
_welcome_label.visible = true
|
|
_set_nav_enabled(true)
|
|
_auth_buttons_row.visible = false
|
|
print("Auth successful. Token stored.")
|
|
await _prime_avatar()
|
|
else:
|
|
# Failure — show backend error detail.
|
|
_show_status(result.detail)
|
|
|
|
|
|
# ─── Avatar priming ───────────────────────────────────────────────────────────
|
|
|
|
## After a successful auth, load the asset catalog and the player's saved
|
|
## loadout so the in-world player renders as their avatar. Best-effort: a
|
|
## failure here just means the player falls back to the default capsule, it
|
|
## never blocks getting into the game.
|
|
func _prime_avatar() -> void:
|
|
if not Engine.has_singleton("AvatarSystem") and get_node_or_null("/root/AvatarSystem") == null:
|
|
return
|
|
var avatars = get_node_or_null("/root/AvatarSystem")
|
|
if avatars == null:
|
|
return
|
|
if not await avatars.ensure_catalog():
|
|
push_warning("main_menu: avatar catalog failed to load; using fallback look")
|
|
return
|
|
var res: Dictionary = await ApiClient.get_avatar_loadout()
|
|
if res.ok and res.data is Dictionary and (res.data as Dictionary).has("data"):
|
|
avatars.set_loadout_from_dict((res.data as Dictionary)["data"])
|
|
elif FileAccess.file_exists("user://loadout.json"):
|
|
var f := FileAccess.open("user://loadout.json", FileAccess.READ)
|
|
if f != null:
|
|
var txt := f.get_as_text()
|
|
f.close()
|
|
var dict: Variant = JSON.parse_string(txt)
|
|
if dict is Dictionary:
|
|
avatars.set_loadout_from_dict(dict as Dictionary)
|
|
elif not avatars.has_loadout():
|
|
# No saved loadout reachable -- fall back to the catalog's first base body.
|
|
var bases: Array = avatars.get_assets_for_slot("base_body")
|
|
if bases.size() > 0:
|
|
avatars.set_loadout_from_dict({"base_body": bases[0], "slots": {}})
|
|
|
|
|
|
# ─── Status helpers ───────────────────────────────────────────────────────────
|
|
|
|
func _show_status(message: String) -> void:
|
|
_status_label.text = message
|
|
_status_label.visible = true
|
|
|
|
|
|
func _clear_status() -> void:
|
|
_status_label.text = ""
|
|
_status_label.visible = false
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event is InputEventKey and event.pressed and not event.echo:
|
|
if event.keycode == KEY_M:
|
|
var focus_owner := get_viewport().gui_get_focus_owner()
|
|
if not (focus_owner is LineEdit or focus_owner is TextEdit):
|
|
var audio_mgr = get_node_or_null("/root/AudioManager")
|
|
if audio_mgr and audio_mgr.has_method("toggle_sound"):
|
|
audio_mgr.toggle_sound()
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
if event.is_action_pressed("ui_cancel"):
|
|
if has_node("ControlsModal"):
|
|
get_node("ControlsModal").queue_free()
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
if _auth_panel != null and _auth_panel.visible and not ApiClient.has_token():
|
|
_auth_panel.visible = false
|
|
_auth_buttons_row.visible = true
|
|
_clear_status()
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
|
func _on_controls_button_pressed() -> void:
|
|
if has_node("ControlsModal"):
|
|
return
|
|
var modal := preload("res://scripts/controls_modal.gd").new()
|
|
add_child(modal)
|