From 62e15d0c872cb3728acb37ff66de12f4da7db474 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Wed, 19 Aug 2026 16:13:30 +0000 Subject: [PATCH] WIP checkpoint: Implementation: Browse worlds screen wired to GET /api/worlds --- scenes/main_menu.tscn | 6 ++ scenes/world_list.tscn | 59 ++++++++++++++++++ scripts/main_menu.gd | 4 +- scripts/world_list.gd | 136 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 scenes/world_list.tscn create mode 100644 scripts/world_list.gd diff --git a/scenes/main_menu.tscn b/scenes/main_menu.tscn index dd4847f..f8b9ed1 100644 --- a/scenes/main_menu.tscn +++ b/scenes/main_menu.tscn @@ -34,10 +34,16 @@ layout_mode = 1 text = "Play" size_flags_horizontal = 4 +[node name="BrowseButton" type="Button" parent="CenterContainer/VBoxContainer"] +layout_mode = 1 +text = "Browse Worlds" +size_flags_horizontal = 4 + [node name="QuitButton" type="Button" parent="CenterContainer/VBoxContainer"] layout_mode = 1 text = "Quit" size_flags_horizontal = 4 [connection signal="pressed" from="CenterContainer/VBoxContainer/PlayButton" to="." method="_on_play_button_pressed"] +[connection signal="pressed" from="CenterContainer/VBoxContainer/BrowseButton" to="." method="_on_browse_button_pressed"] [connection signal="pressed" from="CenterContainer/VBoxContainer/QuitButton" to="." method="_on_quit_button_pressed"] diff --git a/scenes/world_list.tscn b/scenes/world_list.tscn new file mode 100644 index 0000000..78b70fc --- /dev/null +++ b/scenes/world_list.tscn @@ -0,0 +1,59 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://scripts/world_list.gd" id="1_wl"] + +[node name="WorldList" type="Control"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_wl") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 3 +theme_override_constants/separation = 8 +size_flags_horizontal = 4 +size_flags_vertical = 4 + +[node name="HeaderHBox" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 3 +size_flags_horizontal = 4 +theme_override_constants/separation = 16 + +[node name="BackButton" type="Button" parent="VBoxContainer/HeaderHBox"] +layout_mode = 3 +text = "< Back" +size_flags_horizontal = 0 + +[node name="TitleLabel" type="Label" parent="VBoxContainer/HeaderHBox"] +layout_mode = 3 +text = "Browse Worlds" +horizontal_alignment = 1 +size_flags_horizontal = 4 +theme_override_font_sizes/font_size = 28 + +[node name="RefreshButton" type="Button" parent="VBoxContainer/HeaderHBox"] +layout_mode = 3 +text = "Refresh" +size_flags_horizontal = 0 + +[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] +layout_mode = 3 +size_flags_horizontal = 4 +size_flags_vertical = 4 +horizontal_mode = 2 +vertical_mode = 2 + +[node name="ScrollVBox" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"] +layout_mode = 3 +size_flags_horizontal = 4 +size_flags_vertical = 4 +theme_override_constants/separation = 8 + +[node name="StatusLabel" type="Label" parent="VBoxContainer"] +layout_mode = 3 +text = "" +visible = false +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 18 diff --git a/scripts/main_menu.gd b/scripts/main_menu.gd index aa4efc5..c026f5a 100644 --- a/scripts/main_menu.gd +++ b/scripts/main_menu.gd @@ -6,8 +6,8 @@ func _ready(): func _on_play_button_pressed(): - # No gameplay scene exists yet — wire this to the real scene once one lands. - print("Play pressed — no gameplay scene yet") + # Navigate to the world list / browse screen. + get_tree().change_scene_to_file("res://scenes/world_list.tscn") func _on_quit_button_pressed(): diff --git a/scripts/world_list.gd b/scripts/world_list.gd new file mode 100644 index 0000000..7804bed --- /dev/null +++ b/scripts/world_list.gd @@ -0,0 +1,136 @@ +extends Control +## World list screen — browses public worlds via GET /api/worlds. +## +## Displays a scrollable list of world cards (name + description). +## Handles loading, empty list, errors, and refresh. + +const WORLD_CARD_SCENE := preload("res://scenes/world_card.tscn") + +# UI references +@onready var scroll_container: ScrollContainer = $VBoxContainer/ScrollContainer +@onready var world_list: VBoxContainer = $VBoxContainer/ScrollContainer/WorldList +@onready var loading_label: Label = $VBoxContainer/LoadingLabel +@onready var error_label: Label = $VBoxContainer/ErrorLabel +@onready var refresh_button: Button = $VBoxContainer/RefreshButton +@onready var back_button: Button = $VBoxContainer/BackButton +@onready var empty_label: Label = $VBoxContainer/EmptyLabel + + +func _ready() -> void: + refresh_button.disabled = false + back_button.disabled = false + loading_label.visible = false + error_label.visible = false + empty_label.visible = false + world_list.visible = false + _refresh_worlds() + + +func _on_refresh_button_pressed() -> void: + _refresh_worlds() + + +func _on_back_button_pressed() -> void: + get_tree().change_scene_to_file("res://scenes/main_menu.tscn") + + +## Fetch the world list from the backend and update the UI. +func _refresh_worlds() -> void: + # Reset UI state + loading_label.visible = true + error_label.visible = false + empty_label.visible = false + world_list.visible = false + refresh_button.disabled = true + + # Clear previous world cards + for child in world_list.get_children(): + child.queue_free() + + var result := await ApiClient.get_worlds() + + loading_label.visible = false + refresh_button.disabled = false + + if not result.ok: + # Show error message (401, network failure, etc.) + error_label.text = "Error: %s" % result.detail + error_label.visible = true + empty_label.visible = false + world_list.visible = false + return + + var data = result.data + if not data is Array: + error_label.text = "Unexpected response format." + error_label.visible = true + empty_label.visible = false + world_list.visible = false + return + + if data.is_empty(): + empty_label.text = "No worlds yet. Check back later!" + empty_label.visible = true + error_label.visible = false + world_list.visible = false + return + + # Render world cards + world_list.visible = true + empty_label.visible = false + error_label.visible = false + + for world_dict in data: + if not world_dict is Dictionary: + continue + var card := _create_world_card(world_dict) + world_list.add_child(card) + + +## Create a visual card for a single world entry. +func _create_world_card(world_dict: Dictionary) -> Control: + var card := VBoxContainer.new() + card.size_flags_horizontal = Control.SIZE_FILL + card.custom_minimum_size = Vector2(0, 80) + card.theme_override_constants/separation = 4 + + var name_label := Label.new() + name_label.text = world_dict.get("name", "Unnamed World") + name_label.autowrap_mode = TextServer.AUTOWRAP_WORD + name_label.size_flags_horizontal = Control.SIZE_FILL + name_label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + name_label.add_theme_font_size_override("font_size", 18) + + var desc_label := Label.new() + var description := world_dict.get("description", "") + desc_label.text = description if description != "" else "No description." + desc_label.autowrap_mode = TextServer.AUTOWRAP_WORD + desc_label.size_flags_horizontal = Control.SIZE_FILL + desc_label.size_flags_vertical = Control.SIZE_EXPAND_FILL + desc_label.add_theme_font_size_override("font_size", 14) + desc_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7, 1.0)) + + var meta_label := Label.new() + var visibility := world_dict.get("visibility", "public") + var created := world_dict.get("created_at", "") + meta_label.text = "Visibility: %s%s" % [visibility, " | Created: " + created if created != "" else ""] + meta_label.autowrap_mode = TextServer.AUTOWRAP_WORD + meta_label.size_flags_horizontal = Control.SIZE_FILL + meta_label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + meta_label.add_theme_font_size_override("font_size", 12) + meta_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5, 1.0)) + + card.add_child(name_label) + card.add_child(desc_label) + card.add_child(meta_label) + + # Subtle background for the card + var bg := ColorRect.new() + bg.color = Color(0.15, 0.15, 0.15, 1.0) + bg.size_flags_horizontal = Control.SIZE_FILL + bg.size_flags_vertical = Control.SIZE_FILL + bg.custom_minimum_size = Vector2(0, 80) + card.add_child(bg) + card.move_child(bg, 0) + + return card