WIP checkpoint: Address review feedback: Implement Godot dedicated server scene + player spawning

This commit is contained in:
2026-08-20 23:31:27 +00:00
parent 1c3c57dbd0
commit 4276a89a6a
94 changed files with 9220 additions and 0 deletions
+262
View File
@@ -0,0 +1,262 @@
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 _play_button: Button
var _quit_button: Button
var _is_register_mode: bool = false
var _is_authenticating: bool = false
# ─── Lifecycle ────────────────────────────────────────────────────────────────
func _ready() -> void:
# 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
_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
_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
_play_button = $CenterContainer/VBoxContainer/PlayButton
_quit_button = $CenterContainer/VBoxContainer/QuitButton
# Default to Play 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.
_play_button.disabled = true
# 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)
auth_state_ready.emit()
return
# Token missing or invalid — fall through to logged-out state.
_reset_to_logged_out()
auth_state_ready.emit()
## 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
_play_button.disabled = false
_auth_buttons_row.visible = false
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
_email_edit.visible = false
_over_18_check.visible = false
_over_18_check.button_pressed = false
_play_button.disabled = true # Gate play behind successful auth.
_update_auth_mode_ui()
# ─── Play / Quit ──────────────────────────────────────────────────────────────
func _on_play_button_pressed() -> void:
if _play_button.disabled:
return
# Browse worlds is the first real screen past login -- actual gameplay
# (joining a world) doesn't exist yet, browsing does.
get_tree().change_scene_to_file("res://scenes/world_list.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() -> 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)
_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)
_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:
# Success — hide the auth form fields, show only the welcome label.
_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
_play_button.disabled = false
_auth_buttons_row.visible = false
print("Auth successful. Token stored.")
else:
# Failure — show backend error detail.
_show_status(result.detail)
# ─── 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